(this is a crosspost from StackOverflow, it was suggested I asked here)GoalI've got a DOM with about 70 elements on it (divs with some content) . I need to move and toggle the display of those divs quite a lot and also quite fast. The speed is one of the most important things. The trigger for moving and toggling these divs is a search query, kind of like Google Instant, except that all the DOM elements I move around and toggle are loaded the first time (so no more calls to the server).ImplementationI've implemented this in the following way: alongside the DOM I pass in a JavaScript array of objects representing the divs along with their attributes like position, contents etcetera. This array acts like a mirror to the DOM. When the user starts typing I start looping through the array and calculating, per div/object, what needs to be done to it. I actually loop over this array a couple of times: I first check if I need to look at a div/object, then I look at the object, then whether I need to look at the contents, then I look at the contents. One of the things I do in these loops is the setting of flags for DOM-manipulation. As I understand it, reading and manipulating and the DOM is one of the slower operations in JavaScript, as compared to the other stuff I'm doing (looping, reading and writing object attributes etc.). I also did some profiling, confirming this assumption. So at every corner I've tried to prevent touching the DOM to increase performance. At the end of my algorithm I loop once more, execute all the necessary DOM actions and reset the flags to signal they've been read. For cross-browser compatibility I use jQuery to actually do the DOM actions (selecting, moving, toggling). I do not use jQuery to loop over my array.ProblemMy problem now is that I think my code and data structure is a bit ugly. I have this rather large multidimensional array with lots of attributes and flags. I repeatedly loop over it with functions calling functions calling functions. When running into problems I can (still) somewhat easily debug stuff, but it doesn't feel right.QuestionIs there a design pattern or common solution to this kind of problem? I suspect I could implement some sort of smart coupling between the array and the DOM where I would not have to explicitly set flags and execute DOM actions, but I've no idea how such a coupling should work or if it's even a good idea or just complicating things.Are there any other data-structure or algorithmic principles I've overlooked when solving this problem?Thanks!Note: I'm not polluting the global namespace, these functions are defined and used inside a closure./** * Applies the filter (defined by the currentQuery and to the cats array) * * -checks whether matching is needed * -if needed does the matching * -checks whether DOM action is needed * -if needed executes DOM action * * cats is an array of objects representing categories * which themselves contain an array of objects representing links * with some attributes * * cats = (array) array of categories through which to search * currentQuery = (string) with which to find matches within the cats * previousQuery = (string) with previously-typed-in query * * no return values, results in DOM action and manipulation of cats array */function applyFilter(cats,currentQuery, previousQuery) {    cats = flagIfMatchingIsNeededForCats(cats,currentQuery,previousQuery);    cats = matchCats(cats,currentQuery);    cats = flagIfMatchingIsNeededForLinks(cats,currentQuery,previousQuery);    cats = matchLinks(cats,currentQuery);    cats = flagIfDisplayToggleNeeded(cats);    if ( currentQuery.length > 0 ) {        cats = flagIfMoveNeeded(cats);    } else {        // move everything back to its original position        cats = flagMoveToOriginalPosition(cats);    }    // take action on the items that need a DOM action    cats = executeDomActions(cats);}/*** Sets a flag on a category if it needs matching, parses and returns cats** Loops through all categories and sets a boolean to signal whether they * need matching.** cats = (array) an array with all the category-objects in it* currentQuery = (string) the currently typed-in query* previousQuery = (string) the query that was previously typed in** returns (array) cats, possibly in a different state*/ function flagIfMatchingIsNeededForCats(cats,currentQuery,previousQuery) {    var newQueryIsLonger = isNewQueryLonger(currentQuery, previousQuery);    // check if matching is necessary for categories    for (var i = 0; i < cats.length; i++) {        cats[i].matchingNeeded = isMatchingNeededForCat(            cats[i].matches            ,newQueryIsLonger            ,currentQuery.length            ,cats[i].noMatchFoundAtNumChars        );    }    return cats;}/*** Whether the new query is longer than the previous one** currentQuery = (string) the currently typed-in query* previousQuery = (string) the query that was previously typed in** returns (boolean) true/false*/function isNewQueryLonger(currentQuery, previousQuery) {    if (previousQuery == false) {        return true;    }    return currentQuery.length > previousQuery.length}/*** Deduces if a category needs to be matched to the current query** This function helps in improving performance. Matching is done using * indexOf() which isn't slow of itself but preventing even fast processes* is a good thing (most of the time). The function looks at the category,* the current and previous query, then decides whether* matching is needed.** currentlyMatched = (boolean) on whether the boolean was matched to the previous query* newQueryIsLonger = (boolean) whether the new query is longer* queryLength = (int) the length of the current query* noMatchFoundAtNumChars = (int) this variable gets set (to an int) for a *   category when it switches from being matched to being not-matched. The*   number indicates the number of characters in the first query that did*   not match the category. This helps in performance because we don't need*   to recheck the categoryname if it doesn't match now and the new query is*   even longer.** returns (boolean) true/false*/function isMatchingNeededForCat(currentlyMatched, newQueryIsLonger ,queryLength ,noMatchFoundAtNumChars) {    if (typeof(currentlyMatched) == 'undefined') {        // this happens the first time we look at a category, for all         // categories this happens with an empty query and that matches with        // everything        currentlyMatched = true;    }    if (currentlyMatched && newQueryIsLonger) {        return true;    }    if (!currentlyMatched && !newQueryIsLonger) {        // if currentlyMatched == false, we always have a value for        // noMatchFoundAtNumChars        // matching is needed if the first no-match state was found         // at a number of characters equal to or bigger than         // queryLength        if ( queryLength < noMatchFoundAtNumChars ) {            return true;        }    }    return false;}/*** Does matching on categories for all categories that need it.** Sets noMatchFoundAtNumChars to a number if the category does not match.* Sets noMatchFoundAtNumChars to false if the category matches once again.** cats = (array) an array with all the category-objects in it* currentQuery = (string) the currently typed-in query** returns (array) cats, possibly in a different state*/function matchCats(cats,currentQuery) {    for (var i = 0; i < cats.length; i++) {        if (cats[i].matchingNeeded) {            cats[i].matches = categoryMatches(cats[i],currentQuery);            // set noMatchFoundAtNumChars            if (cats[i].matches) {                cats[i].noMatchFoundAtNumChars = false;            } else {                cats[i].noMatchFoundAtNumChars = currentQuery.length;            }        }    }    return cats;}/*** Check if the category name matches the query** A simple indexOf call to the string category_name** category = (object) a category object* query = (string) the query** return (boolean) true/false*/function categoryMatches(category,query) {    catName = category.category_name.toLowerCase();    if (catName.indexOf(query) !== -1 ) {        return true;    }    return false;}/*** Checks links to see whether they need matching** Loops through all cats, selects the non-matching, for every link decides* whether it needs matching** cats = (array) an array with all the category-objects in it* currentQuery = the currently typed-in query* previousQuery = the query that was previously typed in** returns (array) cats, possibly in a different state*/function flagIfMatchingIsNeededForLinks(cats,currentQuery,previousQuery) {    var newQueryIsLonger = isNewQueryLonger(currentQuery, previousQuery);    for (var i = 0; i < cats.length; i++) {        if (!cats[i].matches) { // only necessary when cat does not match            for (var k = 0; k < cats[i].links.length; k++) {                cats[i].links[k].matchingNeeded = isMatchingNeededForLink(                    cats[i].links[k].matches                    ,newQueryIsLonger                    ,currentQuery.length                    ,cats[i].links[k].noMatchFoundAtNumChars                );            }        }    }    return cats;}/*** Checks whether matching is needed for a specific link** This function helps in improving performance. Matching is done using * indexOf() for every (relevant) link property, this function helps decide* whether that *needs* to be done. The function looks at some link * properties, the current and previous query, then decides whether* matching is needed for the link.** currentlyMatched = (boolean) on whether the boolean was matched to the previous query* newQueryIsLonger = (boolean) whether the new query is longer* queryLength = (int) the length of the current query* noMatchFoundAtNumChars = (int) this variable gets set (to an int) for a *   link when it switches from being matched to being not-matched. The*   number indicates the number of characters in the first query that did*   not match the link. This helps in performance because we don't need*   to recheck the link properties in certain circumstances.** return (boolean) true/false*/function isMatchingNeededForLink(currentlyMatched, newQueryIsLonger ,queryLength ,noMatchFoundAtNumChars) {    if (typeof(currentlyMatched) == 'undefined') {        // this happens to a link the first time a cat does not match and        // we want to scan the links for matching        return true;                }    if (currentlyMatched && newQueryIsLonger) {        return true;    }    if (!currentlyMatched && !newQueryIsLonger) {        // if currentlyMatched == false, we always have a value for        // noMatchFoundAtNumChars        // matching is needed if the first no-match state was found         // at a number of characters equal to or bigger than         // queryLength        if ( queryLength < noMatchFoundAtNumChars ) {            return true;        }    }    return false;}/*** Does matching on links for all links that need it.** Sets noMatchFoundAtNumChars to a number if the link does not match.* Sets noMatchFoundAtNumChars to false if the link matches once again.** cats = (array) an array with all the category-objects in it* currentQuery = (string) the currently typed-in query** returns (array) cats, possibly in a different state*/function matchLinks(cats,currentQuery) {    for (var i = 0; i < cats.length; i++) {        // category does not match, check if links in the category match        if (!cats[i].matches) {            for (var k = 0; k < cats[i].links.length; k++) {                if (cats[i].links[k].matchingNeeded) {                    cats[i].links[k].matches = linkMatches(cats[i].links[k],currentQuery);                }                // set noMatchFoundAtNumChars                if (cats[i].links[k].matches) {                    cats[i].links[k].noMatchFoundAtNumChars = false;                } else {                    cats[i].links[k].noMatchFoundAtNumChars = currentQuery.length;                }            }        }    }    return cats;}    /*** Check if any of the link attributes match the query** Loops through all link properties, skips the irrelevant ones we use for filtering** category = (object) a category object* query = (string) the query** return (boolean) true/false*/function linkMatches(link,query) {    for (var property in link) {        // just try to match certain properties        if (                !( // if it's *not* one of the following                    property == 'title'                    || property == 'label'                    || property == 'url'                    || property == 'keywords'                    || property == 'col'                    || property == 'row'                )        ){            continue;        }        // if it's an empty string there's no match        if( !link[property] ) {            continue;        }        var linkProperty = link[property].toLowerCase();        if (linkProperty.indexOf(query) !== -1){            return true;        }    }    return false;}/*** Flags if toggling of display is needed for a category.** Loops through all categories. If a category needs some DOM* action (hiding/showing) it is flagged for action. This helps in * performance because we prevent unnecessary calls to the DOM (which are * slow).** cats = (array) an array with all the category-objects in it** returns (array) cats, possibly in a different state*/function flagIfDisplayToggleNeeded(cats) {    for (var i = 0; i < cats.length; i++) {        // this happens the first time we look at a category        if (typeof(cats[i].currentlyDisplayed) == 'undefined') {            cats[i].currentlyDisplayed = true;        }        var visibleLinks = 0;        // a cat that matches, all links need to be shown        if (cats[i].matches) {            visibleLinks = cats[i].links.length;        } else {            // a cat that does not match            for (var k = 0; k < cats[i].links.length; k++) {                if (cats[i].links[k].matches) {                    visibleLinks++;                }            }                    }        // hide/show categories if they have any visible links        if (!cats[i].currentlyDisplayed && visibleLinks > 0 ) {            cats[i].domActionNeeded = 'show';        } else if( cats[i].currentlyDisplayed && visibleLinks == 0 ){            cats[i].domActionNeeded = 'hide';        }               }    return cats;}/*** Flags categories to be moved to other position.** Loops through all categories and looks if they are distributed properly. * If not it moves them to another position. It remembers the old position so* it can get the categories back in their original position.** cats = (array) an array with all the category-objects in it** returns (array) cats, possibly in a different state*/function flagIfMoveNeeded(cats) {    var numCats, numColumns, displayedCats, i, moveToColumn, tmp;    numColumns = getNumColumns(cats);    numDisplayedCats = getNumDisplayedCats(cats);            columnDistribution = divideInPiles(numDisplayedCats, numColumns);    // optional performance gain: only move stuff when necessary    // think about this some more    // we convert the distribution in columns to a table so we get columns    // and positions    catDistributionTable = convertColumnToTableDistribution(columnDistribution);    // sort the categories, highest positions first    // catPositionComparison is a function to do the sorting with    // we could improve performance by doing this only once    cats = cats.sort(catPositionComparison);    for (i = 0; i < cats.length; i += 1) {        if( categoryWillBeDisplayed(cats[i]) ){            tmp = getNewPosition(catDistributionTable); // returns multiple variables            catDistributionTable = tmp.catDistributionTable;            cats[i].moveToColumn = tmp.moveToColumn;            cats[i].moveToPosition = tmp.moveToPosition;        } else {            cats[i].moveToColumn = false;            cats[i].moveToPosition = false;        }    }    return cats;}/*** A comparison function to help the sorting in flagIfMoveNeeded()** This function compares two categories and returns an integer value * enabling the sort function to work.** cat1 = (obj) a category* cat2 = (obj) another category** returns (int) signaling which category should come before the other*/function catPositionComparison(cat1, cat2) {    if (cat1.category_position > cat2.category_position) {        return 1; // cat1 > cat2    } else if (cat1.category_position < cat2.category_position) {        return -1; // cat1 < cat2    }    // the positions are equal, so now compare on column, if we need the     // performance we could skip this    if (cat1.category_column > cat2.category_column) {        return 1; // cat1 > cat2    } else if (cat1.category_column < cat2.category_column) {        return -1; // cat1 < cat2    }    return 0; // position and column are equal}/*** Checks if a category will be displayed for the currentQuery** cat = category (object) ** returns (boolean) true/false*/function categoryWillBeDisplayed(cat) {    if( (cat.currentlyDisplayed === true  && cat.domActionNeeded !== 'hide')        ||        (cat.currentlyDisplayed === false && cat.domActionNeeded === 'show')    ){        return true;    } else {        return false;    }}/** * Gets the number of unique columns in all categories * * Loops through all cats and saves the columnnumbers as keys, insuring * uniqueness. Returns the number of * * cats = (array) of category objects * * returns (int) number of unique columns of all categories */function getNumColumns(cats) {    var columnNumber, uniqueColumns, numUniqueColumns, i;    uniqueColumns = [];    for (i = 0; i < cats.length; i += 1) {        columnNumber = cats[i].category_column;        uniqueColumns[columnNumber] = true;    }    numUniqueColumns = 0;    for (i = 0; i < uniqueColumns.length; i += 1) {        if( uniqueColumns[i] === true ){            numUniqueColumns += 1        }    }    return numUniqueColumns;}/** * Gets the number of categories that will be displayed for the current query * * cats = (array) of category objects * * returns (int) number of categories that will be displayed */function getNumDisplayedCats(cats) {    var numDisplayedCats, i;    numDisplayedCats = 0;    for (i = 0; i < cats.length; i += 1) {        if( categoryWillBeDisplayed(cats[i]) ){            numDisplayedCats += 1;        }    }    return numDisplayedCats;}/** * Evenly divides a number of items into piles * * Uses a recursive algorithm to divide x items as evenly as possible over * y piles. * * items = (int) a number of items to be divided * piles = (int) the number of piles to divide items into * * return an array with numbers representing the number of items in each pile */function divideInPiles(items, piles) {    var averagePerPileRoundedUp, rest, pilesDivided;    pilesDivided = [];    if (piles === 0) {        return false;    }    averagePerPileRoundedUp = Math.ceil(items / piles);    pilesDivided.push(averagePerPileRoundedUp);    rest = items - averagePerPileRoundedUp;    if (piles > 1) {        pilesDivided = pilesDivided.concat(divideInPiles(rest, piles - 1)); // recursion    }    return pilesDivided;}/** * Converts a column distribution to a table * * Receives a one-dimensional distribution array and converts it to a two- * dimensional distribution array. * * columnDist (array) an array of ints, example [3,3,2] * * returns (array) two dimensional array, rows with cells * example: [[true,true,true],[true,true,true],[true,true,false]] * returns false on failure */function convertColumnToTableDistribution(columnDist) {    'use strict';    var numRows, row, numCols, col, tableDist;    if (columnDist[0] === 'undefined') {        return false;    }    // the greatest number of items are always in the first column    numRows = columnDist[0];    numCols = columnDist.length;    tableDist = []; // we     for (row = 0; row < numRows; row += 1) {        tableDist.push([]); // add a row        // add cells        for (col = 0; col < numCols; col += 1) {            if (columnDist[col] > 0) {                // the column still contains items                tableDist[row].push(true);                columnDist[col] -= 1;            } else {                tableDist[row][col] = false;            }        }    }    return tableDist;}/*** Returns the next column and position to place a category in.** Loops through the table to find the first position that can be used. Rows* and positions have indexes that start at zero, we add 1 in the return * object.** catDistributionTable = (array) of rows, with positions in them** returns (object) with the mutated catDistributionTable, a column and a * position*/function getNewPosition(catDistributionTable) {    var numRows, row, col, numCols, moveToColumn, moveToPosition;    numRows = catDistributionTable.length;    findposition:    for (row = 0; row < numRows; row += 1) {        numCols = catDistributionTable[row].length;        for ( col = 0; col < numCols; col += 1) {            if (catDistributionTable[row][col] === true) {                moveToColumn = col;                moveToPosition = row;                catDistributionTable[row][col] = false;                break findposition;            }        }    }    // zero-indexed to how it is in the DOM, starting with 1    moveToColumn += 1;    moveToPosition += 1;    return {        'catDistributionTable'  : catDistributionTable        ,'moveToColumn'         : moveToColumn        ,'moveToPosition'       : moveToPosition    };}/*** Sets the target position of a category to its original location** Each category in the DOM has attributes defining their original position.* After moving them around we might want to move them back to their original* position, this function flags all categories to do just that.** cats = (array) of category objects** All of the possible return values*/function flagMoveToOriginalPosition(cats) {    for (i = 0; i < cats.length; i += 1) {        cats[i].moveToColumn = cats.category_column;        cats[i].moveToPosition = cats.category_position;    }    return cats;}/*** Execute DOM actions for the items that need DOM actions** Parses all categories, executes DOM actions on the categories that* require a DOM action.** cats = (array) an array with all the category-objects in it** no return values*/function executeDomActions(cats) {    for (var i = 0; i < cats.length; i++) {        var category_id = cats[i].category_id;        // toggle display of columns        if (cats[i].domActionNeeded == 'show') {            showCategory(category_id);            cats[i].currentlyDisplayed = true;        }        if (cats[i].domActionNeeded == 'hide') {            hideCategory(category_id);            cats[i].currentlyDisplayed = false;        }        cats[i].domActionNeeded = false;        // for every currentlyDisplayed category move it to new location        // if necessary        if (cats[i].currentlyDisplayed && cats[i].moveToColumn !== false) {            cats[i] = moveCat(cats[i]);        }    }    return cats;}/*** Show a certain category** category_id = (int) the id of the category that needs to be shown** no return values*/function showCategory(category_id) {    $('#' + category_id).show();}/*** Hide a certain category** category_id = (int) the id of the category that needs to be hidden** no return values*/function hideCategory(category_id) {    $('#' + category_id).hide();}/** * Moves a category to the position set in its attributes * * A category can have attributes defining the column and position (or row) * this function moves the category to the correct column and position. * * cat = (object) category * * returns (object) category */function moveCat(cat) {    var columnSelector, catSelector;    columnSelector = '#column' + cat.moveToColumn + ' .column_inner' + ' .hiddenblocks';    catSelector = '#' + cat.category_id;    $(columnSelector).prepend($(catSelector));    // reset target coordinates    cat.moveToColumn = false;    cat.moveToPosition = false;    return cat;}
DOM-manipulation search-ordering algorithm
javascript;algorithm;jquery
First off...all this cats = and return cats confuses things, since you never actually replace cats -- the items inside are what you mess with.  cats = doSomethingWith(cats) implies to me that doSomethingWith(cats) clones the array and doesn't touch the original.Secondly, you're over-granulating.  The way you're doing things (doing all of one thing, then all of the next), it all but requires your object to wear all kinds of flags to carry information between functions.  However, you could turn something likex(cats)y(cats)z(cats)function x()    for each (item) in cats:        do step 1 to itemfunction y()    for each (item) in cats:        if item.(step 1 result) says so, do step 2function z()    for each (item) in cats:        ...to something likefor each (item) in cats:    do step 1 to item    if (step 1 result) says so, do step 2    ...which would make it possible to keep intermediate results local, and thus eliminate the need for a bunch of your flags.isMatchingNeededForCat and isMatchingNeededForLink do the exact same thing (with the same properties, even!), and should be combined into one function.Also, keep abstraction levels in mind.  At the higher level of applyFilter, you don't particularly care if matching is required for an item.  That's an optimization detail that can be relegated to the point where you're actually trying to match categories, particularly since (assuming previousQuery and currentQuery are similar enough, as the code seems to assume they are) it has little to no effect on the result.  (isMatchingNeeded could always return true if it wanted to, and the code should still work, albeit slower.)With those things fixed, and some rearranging of parameters and such, this is what i have for everything up to and including flagIfDisplayToggleNeeded.  The code's been reduced by about 60% (even more if you don't count comments), and a number of moving parts have been eliminated./** * Applies the filter (defined by `query`) to the cats array * * -finds and flags matching categories, and matching links within categories * -checks whether DOM action is needed * -if needed executes DOM action * * cats is an array of objects representing categories * which themselves contain an array of objects representing links * with some attributes * * cats = (array) array of categories through which to search * currentQuery = (string) with which to find matches within the cats * previousQuery = (string) with previously-typed-in query * * no return values, results in DOM action and manipulation of cats array */function applyFilter(cats, query, previousQuery) {    // previousQuery must be a string (or at least quack like one)    if (previousQuery == undefined || !('length' in previousQuery)) {        previousQuery = '';    }    for (var i = 0; i < cats.length; i++) {        var cat = cats[i];        matchCategory(cat, currentQuery, previousQuery);        var catShouldBeVisible = cat.matches;        if (!cat.matches) {            for (var k = 0; k < cat.links.length; k++) {                matchLink(cat.links[k], currentQuery, previousQuery);                if (cat.links[k].matches) catShouldBeVisible = true;            }        }        if (cat.isCurrentlyDisplayed != catShouldBeVisible) {            cat.domActionNeeded = (catShouldBeVisible ? 'show' : 'hide');        }    }    // Exercise for the reader: see about getting rid of these.  :)    if ( query.length > 0 ) {        flagIfMoveNeeded(cats);    } else {        // move everything back to its original position        flagMoveToOriginalPosition(cats);    }    // take action on the items that need a DOM action    executeDomActions(cats);}/** * Attempts to match the given link to the current query, if necessary. * `cat`'s `matches` and `noMatchFoundAtNumChars` properties will be set accordingly. * * cat           (object) : the category to check.  Will be modified as described below. * query         (string) : the current query. * previousQuery (string) : the previous query, for which `cat` holds valid results. * * No return value.  Modifies `cat` to reflect the results of the match attempt. * The `matches` property will reflect whether the current query matches this item, and * `noMatchFoundAtNumChars` reflects the current query's length if no match was found. */function matchCategory(cat, query, previousQuery) {    if (isMatchingNeeded(cat, query, previousQuery)) {        var catName = cat.category_name.toLowerCase();        cat.matches = (catName.indexOf(currentQuery) !== -1 );        cat.noMatchFoundAtNumChars = (cat.matches ? false : query.length);    }}/** * Attempts to match the given link to the current query, if necessary. * `link`'s `matches` and `noMatchFoundAtNumChars` properties will be set accordingly. * * link          (object) : the link to check.  Will be modified as described below. * query         (string) : the current query. * previousQuery (string) : the previous query, for which `link` holds valid results. * * No return value.  Modifies `link` to reflect the results of the match attempt. * The `matches` property will reflect whether the current query matches this item, and * `noMatchFoundAtNumChars` reflects the current query's length if no match was found. */function matchLink(link, query, previousQuery) {    if (isMatchingNeeded(link, query, previousQuery)) {        var properties = [ 'title', 'label', 'url', 'url', 'keywords', 'col', 'row' ];        link.matches = false;        for (var i = 0; i < properties.length; ++i) {            var property = properties[i];            if (!link[property]) continue;            var linkProperty = link[property].toLowerCase();            if (linkProperty.indexOf(query) !== -1){                link.matches = true;                break;            }        }        link.noMatchFoundAtNumChars = (link.matches ? false : query.length);    }}/** * Returns whether the cached results from the previous query are invalid for the current * query, and thus need to be rechecked. * * item          (object) : the item to check.  If `item.matches` does not yet exist, *                          then `previousQuery` is ignored and the function returns true. * query         (string) : the current query. * previousQuery (string) : the query for which `item`'s current results are valid. * * Return value (boolean) : Whether a match should be retried (and the results saved) for *                          the current query. * * NOTE: If `item.matches` exists, then either `previousQuery` or `query` must be a * substring of the other.  That is, either `previousQuery.indexOf(query)` or * `query.indexOf(previousQuery)` must be >= 0.  If this is not the case, the result is * undefined. */function isMatchingNeeded(item, query, previousQuery) {    // If it's never been checked yet, definitely check it now    if (!('matches' in item)) return true;    // If the query is longer, then existing matches need to be rechecked.    // If not, then existing unmatches may need rechecking.    return (        (query.length > previousQuery.length)        ? item.matches         : (query.length < item.noMatchFoundAtNumChars)    );}Note how i've gotten rid of the flagIfMatchNeededFor*, and condensed their functionality into the appropriate match* functions.  I'm also looping within applyFilter (and calling isMatchingNeeded for each item within match* right before i try matching it) rather than having a bunch of functions that loop over the whole array independently.  Result being, i can keep intermediate results local and thus avoid having to set a flag on the object for every.  single.  step.  of the process.There are other changes that could be beneficial.  See how we're passing a cat or a link to all these functions?  And how like 2/3 of your functions take either a category, a link, or an array of one of the two?  Something tells me there's an opportunity here to arrange this stuff into full-fledged objects (as opposed to the current state of things, where the objects are just being used as associative arrays), and leverage the principles of OOP to simplify the code even more.And yes, you can use DOM objects as regular JS objects to an extent.  (If you do so, though, i'd restrict it to CSS classes and data- attributes for the custom stuff.  Adding your own custom properties generally works, but you'll need to worry about name collisions, among other things.)Oh...while i'm thinking about DOM objects...one more thing that could easily impact performance:  keep an eye on your use of jQuery.  Use it when it's doing something complicated that you don't want to have to do, but be aware that it's doing a lot of extra crap behind the scenes.  For example, when you say $('#' + category_id).hide(), several things are going on:A new string is constructed.$ is called, which calls $.fn.init.jQuery.fn.init uses a regex to check the selector, and notices it's a single ID.If you're lucky, jQuery will call document.getElementById and be done.  If you're not (usually if you're running IE or Opera, which like to confornicate names and IDs), it'll search the document (which is quite a bit slower). You can usually avoid this slow search by ensuring that your elements' names don't match any element's ID.Either way, the result is wrapped up in a new jQuery object (basically, an array-like object) which is returned to you.$.fn.hide is called.It loops over the array of stuff (yes, you're looping over an array, even if it only contains one element!), and calls jQuery.css(element, 'display') on it (in order to get the previous show/hide state).It sets a data attribute/property/whatever for the element, so that it can remember the previous state.Finally, it says element.style.display = 'none'.All that takes time.  I've seen benchmarks that show manipulating the DOM directly as anywhere from 3x to 100x faster than with jQuery, depending on which browser you're using and what you're doing with it.  In fact, the benchmark i just made shows jQuery as being 92% slower than pure vanilla DOM manipulation in Chrome (that is, 8% of the speed; plain JS is 11.5x faster).  If you're spending a lot of time tweaking elements, that adds up.You could get rid of the jQuery overhead quite easily with the show/hide stuff:function showCategory(category_id) {    document.getElementById(category_id).style.display = '';}function hideCategory(category_id) {    document.getElementById(category_id).style.display = 'none';}If you wanted, you could even give your items a domElement property that'd be set to the corresponding element (and pass the whole item to your show/hide functions), and you wouldn't even have to look it up.With all that said about performance, though...as you go through and clean up the code, i'll warn you to beware of the temptation toward premature optimization.  Particularly watch for the I heard X is slow, so i'm gonna do everything i can to avoid X type; it's quite sneaky.  You might find out that you're spending so much time trying to avoid X, that you'd have done better to just do X right off the bat.  I haven't profiled most of this, because frankly, my #1 goal isn't speed -- i'm working to eliminate unnecessary complexity and make the code more maintainable.  However, i will bet that the new code is not significantly slower than the old, will load faster (since there's much less of it), and is a lot less of a pain to work with.