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:
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:
<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:
<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:
<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:
// 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:
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:
for (const product of products) {
$("#table tbody").append("<tr><td>" + product.name + "</td></tr>");
}Show one possible answer
<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>3 rowsThis 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.