Skip to Content
Find the Slow Function With Chrome's Performance Profiler
Advanced
0 likes
Tutorials

Find the Slow Function With Chrome's Performance Profiler

Record one slow click in Chrome's Performance panel, read the Bottom-up tab by self time, fix the function it points at, and record again to prove the fix worked.

Skip ahead: If you already know your way around the Performance panel, jump to "Read the Bottom-up tab".

A page feels slow, and you already have a guess about why. Don't fix the guess. Record a profile, let Chrome tell you which function took the time, fix that one function, and record again to prove it helped. This tutorial does exactly that on a small page, start to finish.

The demo page

A warehouse picking list. There are 5,000 products and 6,000 order lines. Each line only has a SKU, so the page looks up the product name and shelf for every line. Save this as slow.html and open it in Chrome:

<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Picking list</title></head>
<body>
<h1>Picking list</h1>
<button id="build">Build picking list</button>
<p id="result"></p>
<script>
const products = Array.from({ length: 5000 }, (_, i) => ({ sku: "SKU-" + i, name: "Item " + i, shelf: "A" + (i % 40) }));
const lines = Array.from({ length: 6000 }, (_, i) => ({ orderId: 5000 + Math.floor(i / 3), sku: "SKU-" + ((i * 7919) % 5000), qty: 1 + (i % 4) }));

function findProduct(sku) {
  return products.find(product => product.sku === sku);
}

function buildPickingList() {
  const rows = [];
  for (const line of lines) {
    const product = findProduct(line.sku);
    rows.push({ orderId: line.orderId, name: product.name, shelf: product.shelf, qty: line.qty });
  }
  return rows;
}

document.getElementById("build").addEventListener("click", () => {
  const start = performance.now();
  const rows = buildPickingList();
  document.getElementById("result").textContent = `${rows.length} rows in ${Math.round(performance.now() - start)} ms`;
});
</script>
</body></html>

Click Build picking list. On my run it said 6000 rows in 105 ms. Write your number down. You need it later to know if the fix worked.

Record a profile

  1. Open DevTools (F12) and go to the Performance tab.
  2. Untick Screenshots. You don't need them here, and the recording is smaller without them.
  3. Click the record button (the circle), click Build picking list once, then click Stop.
  4. In the overview strip at the top, drag across the small yellow block to zoom into the click.

Keep the recording short. One click, then stop. A 30-second recording of a 100 ms problem is mostly noise.

Read the flame chart

Under Main you see the click as a stack of bars: Event: click, the click handler, buildPickingList, and under it many findProduct bars. Wide bars took a long time. The bars underneath are the functions they called. So you already know the click time is spent inside buildPickingList, but not yet which part of it.

Read the Bottom-up tab

Open the Bottom-up tab under the flame chart. It lists every function by Self time, the time spent in that function's own code, not in the functions it called. This is the column that points at the culprit.

Chrome DevTools Performance panel after recording one click on Build picking list. The page shows 6000 rows in 105 ms. The flame chart shows buildPickingList calling findProduct many times. The Bottom-up tab lists findProduct at the top with 67.8 ms self time, 63.2 percent, and 86.7 ms total time.

findProduct is at the top with 67.8 ms self time and 86.7 ms total. The (anonymous) row at slow.html:13 is the arrow function passed to find(), another 19.2 ms. Together that's almost all of the click. buildPickingList itself has only 1.0 ms of self time. It's slow because of what it calls, not because of its own code.

That's the difference between the two columns. Total time blames the function at the top of the chain. Self time shows where the work happens.

Why findProduct is slow

find() walks the products array from the start until it hits a match. It's called once per line, so 6,000 lines each scan up to 5,000 products. That's millions of comparisons to answer 6,000 lookups. Nothing is wrong with find(). Calling it inside a loop over a big list is the problem.

Fix it with a Map

Build a Map from SKU to product once, then look up by key. Only findProduct changes:

const products = Array.from({ length: 5000 }, (_, i) => ({ sku: "SKU-" + i, name: "Item " + i, shelf: "A" + (i % 40) }));
const lines = Array.from({ length: 6000 }, (_, i) => ({ orderId: 5000 + Math.floor(i / 3), sku: "SKU-" + ((i * 7919) % 5000), qty: 1 + (i % 4) }));

function buildPickingList(findProduct) {
  return lines.map(line => {
    const product = findProduct(line.sku);
    return { orderId: line.orderId, name: product.name, shelf: product.shelf, qty: line.qty };
  });
}

const slow = buildPickingList(sku => products.find(product => product.sku === sku));

const productsBySku = new Map(products.map(product => [product.sku, product]));
const fast = buildPickingList(sku => productsBySku.get(sku));

console.log(fast.length, fast[1]);
console.log("same result:", JSON.stringify(slow) === JSON.stringify(fast));
6000 { orderId: 5000, name: 'Item 2919', shelf: 'A39', qty: 2 }
same result: true

In the page, the change is two lines:

const productsBySku = new Map(products.map(product => [product.sku, product]));

function findProduct(sku) {
  return productsBySku.get(sku);
}

Always check the output is the same before you celebrate. A fast function that returns the wrong rows is not a fix.

Profile again and compare

Reload the page, record the same single click, and open Bottom-up again.

Chrome DevTools Performance panel after the fix. The page shows 6000 rows in 3 ms. The click is now a thin sliver in the timeline. In the Bottom-up tab, findProduct has 1.3 ms self time, and buildPickingList has 2.2 ms total time. Profiling overhead is now the largest row.

The page now says 6000 rows in 3 ms, down from 105 ms. findProduct dropped from 67.8 ms to 1.3 ms of self time, and the click is a thin line on the timeline. The top row is now Profiling overhead, the profiler measuring itself. When that's the biggest thing left, you're done with this click.

💡 Tip: Compare the same action, in the same tab, with the same data. Change one thing between recordings. If you change three things at once, you won't know which one helped.

âš  Warning: Test with realistic data. With 50 products and 60 lines, the slow version also finishes in well under a millisecond, so the profile looks fine and the problem ships. This kind of slowness grows with the size of both lists.

Common mistakes

  • Sorting by Total time and blaming the click handler, because everything runs inside it.
  • Fixing the loop syntax (for instead of forEach) instead of the repeated search inside it.
  • Building the Map inside findProduct, so it's rebuilt on every call and the page gets slower.
  • Skipping the second recording, so you don't actually know the fix worked.

Where in your code do you look something up by id inside a loop? Profile that click first.

About

Profile a picking list that takes 105 ms per click, find the lookup that does the damage, replace a repeated find() with a Map, and confirm it drops to 3 ms. After this, "the page feels slow" has a function name.

Responsible
Mary Q
Last Update
09/08/2026
Reading Time
6 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.