DEV Community

Cover image for Search as you type at 60fps with js-coroutines
Mike Talbot ⭐
Mike Talbot ⭐

Posted on

Search as you type at 60fps with js-coroutines

It's nice to be able to make user interfaces that require the least number of clicks for the user to achieve their goal. For instance, we might want to search a list as we type. The challenge is though, as the list gets bigger there's a chance that the whole user experience will degrade as our JavaScript hogs the main thread stopping animations and making the whole experience glitchy.

This article will show how we can quickly modify a standard search function to use js-coroutines and keep the fully responsive experience with very little extra effort.

Let's say we have a list of 1,000,000 items and we have a text box, as the user types, we'd like to return the first 50 entries that have words that match the words they've typed (in any order).

For this example, we'll use "unique-names-generator" to create a list of nonsense to search on! Entries will look a little like this:

Aaren the accused lime flyingfish from Botswana
Adriana the swift beige cuckoo from Botswana
Enter fullscreen mode Exit fullscreen mode

Our search function is pretty simple:

function find(value) {
    if (!value || !value.trim()) return []
    value = value.trim().toLowerCase()
    const parts = value.split(" ")
    return lookup
        .filter(v =>
            parts.every(p =>
                v.split(" ").some(v => v.toLowerCase().startsWith(p))
            )
        )
        .slice(0, 50)
}
Enter fullscreen mode Exit fullscreen mode

But with 1,000,000 entries the experience is pretty woeful. Try searching the in the screen below for my favourite dish: 'owl rare', and watch the animated progress circle glitch...

This experience is atrocious and we'd have to either remove the functionality or find a much better way of searching.

js-coroutines to the rescue!

With js-coroutines we can just import the filterAsync method and re-write our "find" to be asynchronous:

let running = null
async function find(value, cb) {
    if (running) running.terminate()
    if (!value || !value.trim()) {
        cb([])
        return
    }
    value = value.trim().toLowerCase()
    let parts = value.split(" ")
    let result = await (running = filterAsync(
        lookup,

        v =>
            parts.every(p =>
                v.split(" ").some(v => v.toLowerCase().startsWith(p))
            )
    ))
    if (result) {
        cb(result.slice(0, 50))
    }
}

Enter fullscreen mode Exit fullscreen mode

Here you can see we terminate any currently running search when the value changes, and we've just added a callback, made the function async and that's about it.

The results are much better:

Alt Text

Top comments (6)

Collapse
 
anuraghazra profile image
Anurag Hazra

This is really awesome mike. great work.

btw can we also show a spinner or loading text while the calculations are happening in the background because UX wise i didn't feel right because as a user i did not get any feedback after typing on the input box

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Yes for sure, I've added it to the demo. Basically you just need to set and remove the searching element - you could even have "real" progress I guess. I changed the app to look like this:

export default function App() {
    const [value, setValue] = React.useState("")
    const [list, setList] = React.useState([])
    const [searching, setSearching] = React.useState(false)
    React.useEffect(() => {
        setSearching(true)
        find(value, results => {
            setList(results)
            setSearching(false)
        })
    }, [value])
    return (
        <div className="App">
            <h1>
                <a href="http://js-coroutines.com">js-coroutines</a> and
                1,000,000 entries <CircularProgress color="secondary" />
            </h1>
            <div
                style={{ display: "flex", alignItems: "center", width: "100%" }}
            >
                <div style={{ flexGrow: 1 }} />
                <input
                    style={{ marginRight: 8 }}
                    value={value}
                    placeholder="type a search"
                    onChange={({ target: { value } }) => setValue(value)}
                />
                <div style={{ opacity: searching ? 1 : 0 }}>
                    <CircularProgress size="1.2em" color="primary" />
                    <em
                        style={{
                            marginLeft: 8,
                            fontSize: "80%",
                            color: "#ccc"
                        }}
                    >
                        Searching...
                    </em>
                </div>
                <div style={{ flexGrow: 1 }} />
            </div>
            {!!list.length && <h3>Recommendations</h3>}
            <ul>
                {list.map((item, index) => {
                    return <li key={index}>{item}</li>
                })}
            </ul>
        </div>
    )
}
Collapse
 
ben profile image
Ben Halpern

This definitely has my attention.

Collapse
 
mungojam profile image
Mark Adamson

How come there isn't more focus on using another thread via web workers in JS?

I've come across from using other languages where kicking off jobs in another thread is pretty simple and so I don't understand why it seems to be so niche in the web world. Is it just about browser compatibility?

Super article though, I'll give this a go later

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Hey, totally use another thread when it makes sense. I do. However, moving stuff around in JS is difficult due to the sandboxing. So for instance in my code I do nearly all my processing on a worker thread that can get the data from an IndexedDb database - however, there's a lot of cases where you aren't in your "core" code and then this stuff helps a lot to avoid a glitch.

Collapse
 
miketalbot profile image
Mike Talbot ⭐