- Chrome Web Store
- Manifest V3
- Testing
- Analytics
Chrome extension performance: measure and fix page slowdowns
Chrome extension performance guide: measure what your extension costs every page with Task Manager, Lighthouse and a Playwright test, then fix and monitor it.
- Author
- Extenify Team
- Published
- Reading time
- 14 min read

On this page
Chrome extension performance is easy to ignore because your extension never shows up in your own page speed reports. It shows up in other people's. Every content script you inject runs on the same main thread as the website, so a heavy extension makes every page slower, and users notice: they uninstall, or they leave a one-star review that says "makes Chrome laggy" without naming a cause.
This guide shows how to measure what your extension costs a page, using the tools built into Chrome and an automated Playwright check you can run in CI. It includes our own test results for four ways of loading the same content script, the fixes that actually move the numbers, and how to keep watching performance after a release.
Key takeaways
- Content scripts share the page's main thread. In our test, a 1.4 MB content script injected at
document_startdelayed first paint from 88 ms to 248 ms and added about 180 ms of main-thread work to every page load.- The same script at the default
document_idletiming still added about 150 ms of work and pushed the load event back by about 165 ms. Deferring the heavy part until after load, or loading it only on user action, removed the delay.- Lighthouse flags extensions automatically: its JavaScript execution audit warns when any
chrome-extension://script spends more than 100 ms scripting.- A Playwright test that loads a page with and without your extension catches regressions before users do.
- After release, watch reviews for words like "slow" and "lag" and line them up with your version history.
Why Chrome extension performance matters
Independent measurements show the problem is common at the top of the store. DebugBear tested about 5,000 popular extensions and found that while 86% add less than 50 ms of CPU time per page, several extensions with over a million users add more than half a second to every page view. One AI assistant with 800,000 users added 2.3 seconds of processing to each visit. An academic study of 72 extensions reached a similar conclusion and added an important detail: extensions slow the browser even in situations they were not designed for, such as on sites where they do nothing useful.
Website owners see it too. When Lighthouse finds a chrome-extension:// script with more than 100 ms of scripting time, its JavaScript execution audit prints the warning "Chrome extensions negatively affected this page's load performance. Try auditing the page in incognito mode or from a Chrome profile without extensions." That warning appears in reports that other developers run on their own sites, with your extension's ID in the script URL.
For your extension, performance problems arrive as store signals rather than bug reports: a rating that slips after an update, reviews that mention speed, and a weekly user count that stops growing. That makes performance part of the same monitoring loop as installs and ratings.
Where an extension costs performance
An extension can slow things down in four places. Only the first one touches every page:
| Part | When it runs | What it costs |
|---|---|---|
| Content scripts | On every page that matches matches |
Download from disk, parse and compile, then your code, all on the page's main thread |
| Service worker | When an event wakes it | CPU and memory in a separate process; a worker that never sleeps holds memory all day |
| Popup and side panel | When the user opens them | Open time; slow popups feel broken but do not affect pages |
| Storage and messaging | Whenever you call them | Large chrome.storage reads and chatty message passing add up across tabs |
The content script row is where most of the measurable damage happens, because the cost is multiplied by every tab and every page load. A script that matches <all_urls> with all_frames: true also runs in every iframe, including ads and embedded videos.
How to measure Chrome extension performance by hand
Start with the tools Chrome already ships. Use a clean Chrome profile with only your extension installed, so other extensions do not distort the numbers.
Chrome Task Manager: memory and CPU per extension
Open the Chrome menu, then More tools > Task manager, or press Shift + Esc on Windows and Linux (Chrome keyboard shortcuts). Each extension appears as its own row with memory footprint and CPU. Sort by memory, then browse normally for a few minutes and watch two things:
- Memory that only grows. A service worker or offscreen document that climbs steadily and never drops usually holds references it should release, often caches or listeners added on every event.
- CPU while idle. An extension that uses CPU when you are not interacting with it is running timers or polling. In Manifest V3, the service worker should be asleep most of the time.
DevTools Performance panel: what your content script does to a page
Open a page your content script matches, open DevTools, go to the Performance panel and record a reload. Then:
- In the flame chart, look for long tasks (marked in red) during load.
- Open the Bottom-Up tab and group by URL. Your content script appears under its
chrome-extension://<your-id>/URL with its self time. - Repeat the recording with the extension disabled on
chrome://extensionsand compare the total scripting time and the timing of First Contentful Paint.
Run it on two or three kinds of sites: a light page, a heavy page with a large DOM, and a site where your extension is supposed to do its main job. Extension cost often depends on the page, because code that walks the DOM gets slower as the DOM grows.
Lighthouse: the warning other developers see
Run Lighthouse from DevTools on the same page with your extension enabled. If the Reduce JavaScript execution time audit lists your chrome-extension:// URL, or the report shows the "Chrome extensions negatively affected" warning, your extension is costing more than 100 ms of scripting on that page. The threshold is applied after Lighthouse's CPU throttling multiplier when it simulates a slower device, so in the default simulated mode a smaller amount of real work can trigger it.
An automated Chrome extension performance test with Playwright
Manual checks catch big problems once. A test catches regressions on every build. The script below loads the same page 21 times without your extension and 21 times with it, then compares the medians. It uses the bundled Chromium in Playwright, which can load unpacked extensions, and Chrome DevTools Protocol metrics for main-thread work.
// perf-check.mjs: compare a test page with and without your extension.
import { chromium } from '@playwright/test';
import path from 'node:path';
const EXTENSION = path.resolve(process.argv[2] ?? 'dist'); // folder with manifest.json
const RUNS = 21;
const PAGE = `<!doctype html><html><body><h1>Product list</h1>${
'<article><h2>Item</h2><p>Some text for the item.</p><a href="#">Details</a></article>'.repeat(400)
}</body></html>`;
async function run(withExtension) {
const args = withExtension
? [`--disable-extensions-except=${EXTENSION}`, `--load-extension=${EXTENSION}`]
: [];
const context = await chromium.launchPersistentContext('', { channel: 'chromium', args });
await context.route('https://perf.example.test/**', (route) =>
route.fulfill({ contentType: 'text/html', body: PAGE }),
);
const samples = [];
for (let i = 0; i <= RUNS; i++) {
const page = await context.newPage();
const cdp = await context.newCDPSession(page);
await cdp.send('Performance.enable');
await page.goto(`https://perf.example.test/${i}`, { waitUntil: 'load' });
await page.waitForTimeout(1000); // let deferred work finish
const timing = await page.evaluate(() => ({
fcp: performance.getEntriesByName('first-contentful-paint')[0]?.startTime ?? NaN,
load: performance.getEntriesByType('navigation')[0].loadEventEnd,
}));
const { metrics } = await cdp.send('Performance.getMetrics');
const busy = metrics.find((m) => m.name === 'TaskDuration').value * 1000;
if (i > 0) samples.push({ ...timing, busy }); // the first load warms caches
await page.close();
}
await context.close();
const median = (key) => {
const sorted = samples.map((s) => s[key]).sort((a, b) => a - b);
return Math.round(sorted[Math.floor(sorted.length / 2)]);
};
return { fcp: median('fcp'), load: median('load'), busy: median('busy') };
}
const base = await run(false);
const ext = await run(true);
console.table({ baseline: base, extension: ext });
const BUDGET_MS = 50;
const extra = ext.busy - base.busy;
if (extra > BUDGET_MS) {
console.error(`Extension adds ${extra} ms of main-thread work per page load (budget ${BUDGET_MS} ms)`);
process.exit(1);
}
Run it with node perf-check.mjs path/to/your/built/extension. The script exits with an error when your extension adds more than 50 ms of main-thread work per page load, which matches the threshold DebugBear used for "minimal impact". A few notes from running it:
- Serve the test page from
context.route. A fixed page keeps the numbers comparable between runs and does not depend on the network. Your content script must match the test URL; with specificmatches, change the URL to one of yours and route it the same way. - Use medians and many runs. Single page loads vary by tens of milliseconds. With 21 loads per side, our three repeated runs gave the same picture for every variant, with one exception covered below.
- Test a page your extension actually works on. If your content script waits for a specific element, add it to the test page, otherwise you only measure the early exit.
Test results: four ways to load the same content script
To see how much loading strategy matters, we built one deliberately heavy content script: a 1.4 MB bundle (9,000 small functions, a stand-in for a bundled UI library) plus an init function that reads the layout of every element on the page and adds a badge. We loaded it four ways and measured each on a 1,600-element page with Playwright 1.64 and Chromium 156, 21 page loads per variant:
| Variant | First Contentful Paint | Load event | Main-thread work | Extra work vs no extension |
|---|---|---|---|---|
| No extension | 88 ms | 38 ms | 63 ms | none |
run_at: document_start |
248 ms | 217 ms | 246 ms | +183 ms |
run_at: document_idle (default) |
80 ms (64 to 256) | 204 ms | 211 ms | +148 ms |
| Idle, heavy part deferred until after load | 76 ms | 53 ms | 119 ms | +56 ms |
| Idle, heavy part loaded only on user action | 76 ms | 64 ms | 55 ms | none until used |
What the numbers show:
document_startis the worst case. The script runs before the page has built its DOM, so the browser cannot paint until it finishes. First paint was delayed on all 21 loads.document_idlehelps less than its name suggests. Chrome's documentation says the browser chooses a time betweendocument_endand just afterwindow.onloadfor idle scripts. On our fast test page that time sometimes fell before the first paint and sometimes after it: first paint ranged from 64 ms to 256 ms, and in one earlier run of 21 loads the median was 220 ms. The load event was delayed every time, and the total work barely changed.- Deferring the heavy part removed the delay. A small content script waited for the
loadevent andrequestIdleCallback, then imported the bundle withimport(chrome.runtime.getURL('heavy.js')). First paint and load matched the baseline. The work still happened, and in this setup it cost less than half as much as injecting the same code directly. - Loading on demand costs nothing until it is used. If a feature only matters when the user clicks your toolbar button or presses a shortcut, do not inject it on every page.
Your numbers will differ with your code, your users' hardware and the sites they visit, so treat the table as a comparison between strategies, not as a benchmark for your extension. The test script above will give you yours.
Fixes that make a Chrome extension faster
Work through these roughly in order of impact:
- Narrow
matches. If your extension only works on a few sites, list them instead of<all_urls>. Pages you do not match pay nothing. - Keep the default
document_idleunless you truly needdocument_start. Only code that must run before the page's own scripts, such as blocking or rewriting something, belongs atdocument_start, and that code should be tiny. - Split the content script. Inject a small loader that decides whether this page needs you, then load the heavy part with a dynamic
import()of aweb_accessible_resourcesfile after load or on user action. - Inject on demand. For features triggered by the toolbar button, use
chrome.scripting.executeScriptfrom the click handler. With theactiveTabpermission you do not even need host permissions for this. - Avoid forced layout in loops. Reading
getBoundingClientRect()oroffsetHeightfor many elements forces the browser to calculate layout. Batch reads before writes, and useIntersectionObserverinstead of measuring elements yourself. - Throttle
MutationObserverwork. Observers on busy pages fire constantly. Narrow the observed subtree, and batch the handling withrequestAnimationFrameor a short timeout. - Leave
all_framesoff unless you need iframes. It defaults tofalsefor a reason. - Let the service worker sleep. Chrome stops an idle extension service worker after 30 seconds of inactivity, and that is a feature. Store state in
chrome.storage, usechrome.alarmsfor periodic work (30 seconds is the minimum period since Chrome 120), and avoid keep-alive tricks. If you stream long AI responses, our guide to streaming AI responses in a Chrome extension shows how to do it within the lifecycle rules.
After each fix, rerun the Playwright check. A performance budget in CI turns "we made it faster once" into "it stays fast".
Monitor Chrome extension performance after release
Lab tests use your hardware and your test page. Your users have slower laptops, hundreds of tabs and sites you never tested. Three sources of field data help.
Time your own startup work. Wrap your content script's initialization in performance.now() calls and report the duration with your analytics, as an aggregate rather than per page:
// content.js: measure your own init and hand the number to the service worker
const started = performance.now();
init(); // your existing setup code
chrome.runtime.sendMessage({ type: 'perf', name: 'content_init', ms: Math.round(performance.now() - started) });
In the service worker, keep a running count and a few percentiles per day and send those with your normal analytics events, instead of one event per page view. Report the URL only as "matched" or "not matched", never the address itself, so the metric does not become browsing history. The Chrome extension analytics tools comparison covers where to send events from Manifest V3.
Two findings from our tests affect how you do this. First, performance.mark() and performance.measure() calls made in a content script land on the page's own performance timeline: the page could read our myext:init measure with performance.getEntriesByType('measure'). Websites with real-user monitoring may collect those entries, so use performance.now() for your own metrics, or give marks neutral names. Second, the page did record a 320 ms long animation frame caused by our content script, but its scripts list was empty in Chromium 156. Site owners see the slow frame without your extension's name on it, and you cannot rely on that API to attribute your own cost either.
Watch reviews for performance words. Users describe performance problems in reviews before they describe them anywhere else: "slow", "lag", "freezes", "high CPU", "memory", "crashes my tabs". Set up Chrome extension review notifications and search new reviews for those words after each release. Our guide to app review analysis with an LLM shows how to label them automatically.
Line it up with releases. A performance regression almost always starts with a version. Keep your Chrome extension version history next to your ratings and weekly users, and check both in the week after every release. If uninstalls rise while installs hold, read the retention and uninstall guide before blaming the listing.
Extenify brings the store side of that loop into one place: it tracks your extension's users, rating, reviews and versions across the Chrome Web Store, Edge Add-ons and Firefox Add-ons, and sends extension review monitoring alerts to email, Slack, Telegram, Discord or Teams when a new review lands or the rating moves after an update. Every plan starts with a 7-day free trial, no credit card required; see pricing.
Frequently asked questions
Do Chrome extensions slow down page loading?
They can. Content scripts run on the page's main thread, so their parsing and execution delay the page. Most extensions add little: DebugBear found 86% of popular extensions add under 50 ms of CPU time per page. A few heavy ones add more than half a second to every page view.
How do I check how much memory a Chrome extension uses?
Open Chrome's Task Manager from More tools > Task manager, or press Shift + Esc on Windows and Linux. Each extension has its own row with its memory footprint and CPU usage. Sort by memory to find the heaviest ones.
Does document_idle make my content script faster?
It makes it less likely to block the first paint, and it is Chrome's default and recommended timing. It does not reduce the work itself. In our test, a heavy script at document_idle still added about 150 ms of main-thread work and delayed the load event. Deferring or lazy-loading the heavy part is what removed the cost.
Why does Lighthouse say Chrome extensions affected my page's performance?
Lighthouse shows that warning when any script with a chrome-extension:// URL spends more than 100 ms scripting during the audit. Run the audit in an incognito window or a clean Chrome profile to measure your site without extensions.
How do I find which extension is slowing down Chrome?
Open Chrome's Task Manager and sort by CPU and memory. Then disable extensions one at a time on chrome://extensions and reload a slow page to confirm which one causes the slowdown.
What is a good performance budget for a Chrome extension?
A practical budget is under 50 ms of extra main-thread work per page load on a typical page, with no delay to first paint. Measure it with an automated test so every release is checked against the same page.
Summary
Chrome extension performance is measured on other people's pages, so it needs deliberate testing. Use Chrome's Task Manager for memory and CPU, the Performance panel and Lighthouse for what your content script does to a page, and a Playwright check with a budget to catch regressions automatically. In our tests, the loading strategy mattered more than anything else: document_start delayed first paint on every load, document_idle still delayed the load event, and deferring or lazy-loading the heavy code removed the delay. After release, time your own init work, watch reviews for performance complaints and read them against your version history, so a slow update is caught in days rather than in your rating.
Image credits
- Cover photo: Car speedometer by Mickaël Gris on the WordPress Photo Directory, CC0.


