Himanshu SharmaSoftware engineer
Let’s talk

jQuery · Lesson 41 of 45

Writing fast jQuery code

Make jQuery code faster by caching selections, using fast selectors, inserting in one batch, delegating events, avoiding layout thrashing and debouncing.

12 minjQuery todayUpdated 23 October 2026

By the end of this lessonSpot and fix the common causes of slow jQuery pages, and measure the difference instead of guessing.

Measure first

Most jQuery code is fast enough. Slow pages usually come from a few specific patterns, repeated many times: in a loop, in a scroll handler, or over a big list. Before changing anything, measure. The browser's Performance panel (in the developer tools) records exactly where time goes, and for quick checks you can time code yourself:

javascript
const start = performance.now();
// ...the code you are testing...
console.log("took", (performance.now() - start).toFixed(1), "ms");

The examples below time themselves. Run them a few times: exact numbers vary with your computer and browser, but the comparison stays clear.

Cache your selections

Every $("selector") searches the document. Inside a loop or a handler that runs often, searching again and again adds up:

html
<ul id="list"></ul>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  const time = (label, fn) => { const t = performance.now(); fn(); console.log(label, (performance.now() - t).toFixed(1), "ms"); };

  time("search every time:", () => {
    for (let i = 0; i < 2000; i++) $("#list").attr("data-n", i);
  });

  time("cached selection:", () => {
    const $list = $("#list");
    for (let i = 0; i < 2000; i++) $list.attr("data-n", i);
  });
</script>

The cached version skips 1,999 searches. Chaining is a form of caching too: $("#msg").text("Saved").addClass("ok") searches once.

Use fast selectors

Selectors that the browser understands natively (ids, classes, tags, attributes) are matched by its fast built-in engine. jQuery-only selectors such as :visible, :hidden, :contains() or :first must be checked in JavaScript, element by element.

  • Prefer $("#products").find(".price") to $(".price:visible").
  • Narrow first, then filter: $("#products .item").filter(":visible").
  • Start from a cached container inside widgets: $widget.find(".title"), not a page-wide search.

Insert in one batch

Each time you add something to the page, the browser may need to recalculate styles and layout. Appending in a loop makes it do that work many times:

html
<ul id="a"></ul><ul id="b"></ul>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  const time = (label, fn) => { const t = performance.now(); fn(); console.log(label, (performance.now() - t).toFixed(1), "ms"); };

  time("append in a loop:", () => {
    for (let i = 0; i < 3000; i++) $("#a").append($("<li>").text("Item " + i));
  });

  time("build, then append once:", () => {
    const items = [];
    for (let i = 0; i < 3000; i++) items.push($("<li>").text("Item " + i)[0]);
    $("#b").append(items);
  });
</script>

Building plain elements in an array and inserting them with one .append() is consistently faster, and the difference grows with page complexity. For very large lists, .detach() the container, change it, and insert it back, so the browser does not lay out every intermediate step.

Avoid layout thrashing

Reading a size (.width(), .offset(), .scrollTop(), .outerHeight()) forces the browser to finish any pending layout first. Alternating reads and writes forces a fresh layout on every turn of the loop:

html
<div id="boxes"></div>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  for (let i = 0; i < 300; i++) $("#boxes").append("<div class='b' style='width:50px;height:4px;background:#93c5fd'></div>");
  const $boxes = $(".b");
  const time = (label, fn) => { const t = performance.now(); fn(); console.log(label, (performance.now() - t).toFixed(1), "ms"); };

  time("read, write, read, write:", () => {
    $boxes.each(function () { $(this).width($(this).width() + 1); });
  });

  time("all reads, then all writes:", () => {
    const widths = $boxes.map((i, el) => $(el).width()).get();
    $boxes.each(function (i) { $(this).width(widths[i] + 1); });
  });
</script>

The second version reads every width first (one layout), then writes every width (one more layout later). The rule: batch your reads, then batch your writes.

One handler instead of many

Attaching a handler to each of 1,000 rows costs time when the page loads and memory while it runs. One delegated handler on the table does the same job:

javascript
// 1,000 handlers
$("#orders tr").on("click", openOrder);

// 1 handler, and new rows work too
$("#orders").on("click", "tr", openOrder);

Calm down frequent events

scroll, resize, mousemove and input can fire dozens of times a second. Keep their handlers small, and use debouncing (wait until the events stop) or throttling (run at most every so often) for anything heavy:

javascript
let timer;
$(window).on("resize", function () {
  clearTimeout(timer);
  timer = setTimeout(recalculateLayout, 150);
});

For "is this element on screen yet?" checks, the browser's IntersectionObserver replaces scroll handlers entirely and costs almost nothing.

Animate the cheap properties

Animating width, height, top or left makes the browser recalculate layout on every frame. CSS transitions on transform and opacity usually avoid that, which is why the CSS transitions lesson recommends them for frequent effects.

Load less

Performance also means download size. Load jQuery once, minified, from one place. Use the slim build if you do not need Ajax, effects or Deferreds. Remove plugins you no longer use; old sites often carry several that no page calls any more.

Common mistakes

  • Selecting inside loops and handlers instead of caching.
  • Appending one element at a time to big lists.
  • Mixing size reads and style writes in a loop.
  • Heavy work in scroll or resize handlers without debouncing.
  • Optimising without measuring, and making code harder to read for no real gain.

Exercise

This code is slow on large lists. Rewrite it so it searches the page once and inserts the rows in one call:

javascript
for (const product of products) {
  $("#table tbody").append("<tr><td>" + product.name + "</td></tr>");
}
Show one possible answer
html
<table id="table"><tbody></tbody></table>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  const products = [{ name: "Lamp" }, { name: "Desk" }, { name: "Chair" }];
  const $tbody = $("#table tbody");
  const rows = products.map(product => $("<tr>").append($("<td>", { text: product.name }))[0]);
  $tbody.append(rows);
  console.log($tbody.children().length, "rows");
</script>
result
3 rows

This version is also safe: setting text instead of building an HTML string means a product name containing markup cannot inject code.

Key points

  • Store a selection in a variable instead of searching the page again and again.
  • Start selectors with an id or use .find() from a cached container; avoid jQuery-only pseudo-selectors in hot code.
  • Build many elements first and insert them in one call; each insertion can make the browser recalculate the layout.
  • Use one delegated handler instead of hundreds, debounce frequent events, and measure with performance.now() or the browser's profiler.

Check yourself

1. Why store $("#cart") in a variable when you use it five times?
2. Which is usually fastest?
3. What is layout thrashing?
4. What should you do before optimising code?