JavaScript: Having Performance Problems? There is No Library for That

Posted on | ~7 mins
coding javascript

This post touches on a few learnings I had:

  • First, that our intuition about where the performance bottlenecks are is not always right.
  • Second, third party libraries are not always the best solution for your problem, no matter how convenient it feels and how much you want to trust them. Corollary: Measure performance before and after using them to identify bottlenecks.
  • Third, sometimes your own naive and inefficient solutions are just good enough and no optimisation is required.

Being quite new to the whole world of web development (mostly having done backend), I was about to write a post on how awesome the state of the open ecosystem is now. Almost any problem you might have, the Node Package Manager (NPM) repo has a solution for it. I was even starting to think that web programming has become composing existing components and libraries rather than reinventing the wheel from scratch. And to a certain degree that is true. Today is probably the best time to learn new technologies.

My Goals

For this practice project, I wanted to build a fast search that loads data from a JSON and then does the complete searching in the browser without asking the backend for it.

Imagine the data as 1000 titles from Hackernews in the following format:

{
	'title':  "AMA: NY AG Schneiderman on net neutrality and protecting our voice in government",
	'points': 1116,
	'comments': 268

}

I wanted to do a full text search on the ‘title’, ‘number of points’ and ‘number of comments’. (I did not search the real discussion content or comments.) Matching partial words would also be nice. Let’s say I enter (case gets ignored) "vern ny schnei prot 111 68", the post above should show up.

Also, the data needed to be displayed in a table and the search results sortable by ‘points’ and ‘comments’.

Building the Search

Obviously, you don’t want to write your own search. How foolish would that be, right? Therefore, after some research, quite a long list of possibilities came up:

At first glance, Fuse.JS seemed perfect. How to set it up is explained on their website very well. It was fast and worked well, but not to the extent that I needed. Sometimes, I searched for the exact number of comments and the result did not show up or somewhere below others others that I deemed not relevant. The other libraries did not seem to have a match for partial words. Probably I did not get the options right, but never managed to achieve the result I was looking for (which is not to say that those libraries aren’t awesome).

So before signing up for Algolia, which seems to do a nice job for Hackernews search, I decided to give it a try and just implement a stupid search with O(n^2).

What I was doing is to tokenize each search term to an array ("vern ny schnei prot 111 68" => ["vern", "ny", "schnei", "prot", "111" "68"],) and then check for each of those terms if they are a substring of the long search term which is a concatenation of the three fields in the JSON ("AMA: NY AG Schneiderman on net neutrality and protecting our voice in government 1116 268").

The code looked like this:

function search(text){
  const terms = text.toLowerCase().trim().split(" ");
  const filter = matchAll(terms);
  
  return mainJSON.filter(filter);
}

function matchAll(terms){
  return function (json) {
    for (term of terms){
      if (!json.searchString.includes(term)){
        return false;
      }
    }
    return true;
  }
}

That worked surprising well despite the double-loop and was a very happy surprise for me. Instant results on my laptop and mobile. Stupidest and slowest solution is good enough? Perfect. Moving on.

Displaying the Data

Need a table? There is a library for that. In this case I was super happy the amazing features of Bootstrap Table. Well maintained (even for Bootstrap 4), data sorting, passing of data into a variable, nice binding by just specifying which fields to take, easy formatting options, detail view. All I was wishing for.

Once the search results come in, one just calls this simple line and the new table gets rendered:

$('#table').bootstrapTable('destroy').bootstrapTable({
   data: searchResult
 });

Neat and clean.

The Nexus 5 Dismay

I was happy and showing a friend of mine the website. It had worked perfectly fast on my laptop and iPhone 6s, so I was hoping it would anywhere else. He got out his old Nexus 5 and opened the website and it was unbearably slow. After entering any letter (the search was triggered automatically via keyUp event), the whole phone froze for 3-5 seconds each time. I was hoping that it was just a single case but a Galaxy 3 of another friend confirmed that there was a problem. The final nail in the coffin was that another friend with a Google Pixel XL( 4GB of RAM and a quad-core processor) told me that it wasn’t 100% fluent either.

Performance Testing

As my friend and I were sure that the inefficient search is the culprit, I decided to see if I can find a faster way to search and add some logging around the search function and and related parts such as the table draw function (yes, to be fair, there are also libraries for that). Also, I created a regex implementation for my search in hope that it might be faster:

function search(text){
  const terms = text.toLowerCase().trim().replace(" ", "|");
  
  regexpr = new RegExp(terms);
  console.log("reg ex:" + regexpr);
  const filter = matchAll(regexpr);
  
  return mainJSON.filter(filter);
}

function matchAll(regexpr){
  return function (json) {
 	  return json.searchString.search(regexpr) >= 0 ? true : false;
  }
}

Laptop Results

The results of my performance test on my laptop were quite a surprise. The search took in all cases never more than 3 milliseconds. In most cases merely 1 millisecond. Both my naive implementation as well as the regex, no difference.

However, the logging, that I had added just for completeness for the Bootstrap table, showed quite the opposite. When there were many search results (i.e. I searched for an empty string so that nothing got filtered), it took up to 624 milliseconds to render the table. And over half a second is a lot. With little data it was taking “only” 142 millis.

Being quite surprised, I wanted to test if it always takes so long to render a table in general. Having used HandlebarsJS in this practice project already for some other part, I decided to give it a try with a simple template instead.

<script id="result-table-template" type="text/x-handlebars-template">
  <table class="table table-striped">
    <thead>
      <tr>
        <th>Title</th>
        <th>Points</th>
        <th>Comments</th>
      </tr>
    </thead>
    <tbody>
      {{#each posts}}
      <tr>
        <td>{{this.title}}</td>
        <td>{{this.points}}</td>
        <td>{{this.comments}}</td>
      </tr>
      {{/each}}
    </tbody>
  </table>
</script>

In order to use the template, one needs to compile it and then you can call it with your search result array.

const simpleTable = Handlebars.compile($("#result-table-template").html());

// Then, inside the search function:
$("#table").html(simpleTable({posts: searchResult}));

The new table was not having any nice features, but it took never longer than 100 ms to render the table when it was fully populated (with all 1000 results). This is a factor of 6 faster than the Bootstrap table. With few search results, it just took a single digit number to render the table.

Yes, the table can’t do anything yet, but I am optimistic that I can add the features that I need with JavaScript and that they will not be on the rendering path.

The Nexus 5 Delight

To my happiness, the new version rendered fluently on the Nexus 5. No more freezes. The search time was only 3-13ms. I did not measure the new table though as there was no need. Also, an idea for further optimisation came: If I display the first 25 results and add a button to load more or automatically load more when you scroll down, it should make it even more fast (yes, am rather the last to think of that).

Conclusion

This little practice project afforded me several nice learnings. Your own solution can be really good enough, don’t optimise when you don’t have a performance problem.

However, it is really hard to predict what will actually cause performance bottlenecks and it can go against your intuition. So go measure instead if you see a problem.

Further, while there is a library for everything, you have to consider whether you want to use it. Not only do you incur some penalty for fetching and rendering it (see The Cost Of JavaScript), but may also cause bottlenecks during runtime. So be careful.