I gave this a try using a WebWorker. The WebWorker helped out, I think, mostly by creating a time-window for additional text-input to be entered. Then, when a search result is recieved and it doesn't match the latest text-input, the search result is ignored. That prevents the DOM manipulation, which is what slows everything down.
For testing, I'm working with ~75k items. It's kinda spotty on FF, but works pretty well in Chrome and Safari. Even works on the iPhone, although some queries, like 'i' which returns ~50k items, can take ~10 seconds to render (but cached can be down to ~550ms). (I have to admit, my method for measuring these times is in need of improvement.)
A couple of things I did in trying to improve the performnce:
- Worked with
<ul>
s instead of <li>
s, per Alexey's answer
- Worked with an array of strings instead of the
<li>
s, per seand's answer
- The
<ul>
s use absolute positioning
- The full list is never deleted, it's moved off screen
- The
<ul>
s for the last 7 searches are kept attached to the DOM and moved off-screen
- The removal of a
<ul>
(when it's not in the last 7) is done 1 second after it's no longer needed
Here is an excerpt of the code:
// keep a record of the last 7 searches
recents.setMaxLen(7);
$inTx.on('keyup.search-ftr chnage.search-ftr', function() {
var tx = $inTx.val(),
recent;
// skip if same query
if (tx === lastQuery) {
return;
}
// full list is never removed from the DOM, just moved off screen
// so, for the empty search move the full list back on screen
if (tx === '') {
++curSearchId;
// $curList is whatever search results are showing
$curList.css('left', -9999);
$ulWtihAll.css('left', 10);
// $count shows is text of current # showing
$count.text(fullCount);
lastQuery = tx;
$diffSearch.text('search: ');
$diffRender.text('render: ');
return;
}
// have a non-empty search, so move full list off screen if needed
if (lastQuery === '') {
$ulWtihAll.css('left', -9999);
}
lastQuery = tx;
// the `<ul>` elements for last 7 searches are cached
// `recents` is a map (assoc array) capped at length 7
recent = recents.getAndRenew(tx);
if (recent) {
useRecent(recent);
return;
}
// start a search - in web worker if have it
$ind.css('visibility', 'visible');
searchStartMs = +new Date();
searchFunc({searchStr: tx, createStr: true, id: ++curSearchId});
});
function searchCallback(e) {
// ignore if not the results for most recent search
if (e.data.id !== curSearchId) {
return;
}
lastResultId = e.data.id;
renderStartMs = +new Date();
// update the counter
$count.text(e.data.count);
$diffSearch.text('search: ' + (+new Date() - searchStartMs));
$ind.css('visibility', 'hidden');
// DOM adding / removal is the slowest part (esp removal) so
// move the current (now old) search result off screen instead
// of removing it
$curList.css('left', -9999);
// create the new `<ul>` and add it to the DOM
$curList = $('<ul>' + (e.data.result || '') + '</ul>').css(onCSS);
$holder.append($curList);
// add the latest <ul> to the recents capped map
// if something was kicked out of the map, the .add()
// method will return it
var removed = recents.add(e.data.searchStr, {
$elm: $curList,
count: e.data.count
});
// if am item was kicked out of the `recents` map,
// schedule it for removal from DOM
if (removed) {
setTimeout(function() {
removed.$elm.remove();
}, 1000);
}
// update the height of the div that holds the <ul>s
// (can't seem to get this to work right in FF)
setTimeout(function() {
$holder.css('height', 100 + $curList.height());
$diffRender.text('render: ' + (+new Date() - renderStartMs));
}, 0);
}
// the search matches one of the cached <ul>s
function useRecent(recent) {
// move the current list off screen, know it won't
// be removed from the recent set bc not adding
// anything new to the recent set
$curList.css('left', -9999);
$count.text(recent.count);
$curList = recent.$elm;
$curList.css('left', 10);
$diffSearch.text('search: (cached)');
renderStartMs = +new Date();
setTimeout(function() {
$holder.css('height', 100 + $curList.height());
$diffRender.text('render: ' + (+new Date() - renderStartMs));
}, 0);
}
function workerSearch(params) {
worker.postMessage(params);
}
function localSearch(params) {
var res = findMatches(params.searchStr, true);
searchCallback({
data: {
id: curSearchId,
result: res.resultStr,
count: res.count
}
});
}
A demo: http://jsfiddle.net/bWtpy/38/
The full code is a bit demonic, but I wanted to give it a shot.