The first thing you need to do is to stop listening to all the events the input element emits. The cleanest way to do this is, in the dev console:
var current = $("input") var clone = current.cloneNode() current.parentElement.replaceElement(clone, current)
Then paste in this script, set
clone.oninput = on_input, and try typing again. You’ll notice immediately that it’s significantly more responsive.
It’s remarkable how much better my quick and dirty hack is, because of its brute force approach to the problem, its brevity, and how few optimisations were made; it is a mere 28 SLOC and most functions are a single line and very self-explanatory. I’ve been studying the functional programming paradigm recently, and I consider brevity its greatest feature. Calculus, “purity”, testing, insane type systems and all the rest only partially resolve the fundamental problems with programming; the only true way you can know a function is correct is if it’s so small and trivial it cannot be incorrect.
If you’ve been working strictly in imperative code, the implementation of
includes might seem weird, because it is curried, or a generator, if you prefer.
const includes = y => x => x.includes(y)
redditor_usernames.filter(includes("throwaway")) and enables writing code in a declarative manner.
For example, consider the logic for querying the elements. I want
death note and
note death to both resolve to
Death Note, and I also want to ignore capitalisation, punctuation, or really anything that isn’t alphanumeric. The way you do this is for the query and the data to both be in the same format: an array of lower case, alphanumeric words. Then if all of the words in the query are a word or a subword of at least one word in the data, it is a match.
Instead of writing a loop to sanitise input, you can declare what sort of input you like and let the environment do the dirty work for you. It’s much less likely you’ll make a mistake that way and it’s easier adding arbitrary steps. The whole thing is 4 lines:
const to_words = x => x.replace(/\W/g, " ") .split(" ") .filter(nonempty) .map(lower_case)
The core logic of the algorithm can thus be reduced to just:
const multiple_included = xs => x => xs.some(includes(x)) query.every(multiple_included(words))
I have made one optimisation, which is caching parsed data on first use. The user will presumably write several letters, so there’s no reason to loop through hundreds and hundreds of
innerText and sanitise them anew every time. You can maintain a list of tuples like
[node, words], and loop through that instead of walking the node tree.
I’ve also made a further optimisation, although it isn’t too visible for a dataset this small. Even with cached results, it’s a little counter-intuitive that you have to search literally all text to find what you want, and also that the search is so intolerant of errors; is “deaathnote” really that different from “death note”? You can indeed split the data into trigrams and filter out results that are too dissimilar. That solution is more complex because of its data structures, but it’s still a mere 63 lines.
I will again remind you that these aren’t actually optimised at all; functional programming patterns carry a very well-documented performance hit with them, so you could write much tighter, imperative loops full of small gains here and there that would add up to something significant.
Nevertheless, the reasoning for using bloated, hipster caffeinated libraries and frameworks is because they make your core business logic much leaner and easier to write. I think that argument is demonstrably bullshit, as I managed to implement fuzzy full text search in 63 lines, and a less tolerant but still acceptably functional exact match search in a laughable 28. I want to state it very clearly so that all webshits in the world can understand it:
LITERAL, UNIRONIC, AND COMPLETELY NON-METAPHORICAL GAS CHAMBERS FOR EVERY SINGLE JS FRAMEWORK. YES, ALL FRAMEWORKS.
Thank you for coming to my TED Talk and go kill yourself.