Skip to content

Does Adding Analytics Slow Down Your Chrome Extension?

7 min readModerok team

Analytics does not slow down a Chrome extension much, but it costs you something in four places. Here is what each one is and how to measure it.

Analytics should not measurably slow down a Chrome extension, provided the SDK is small, has no runtime dependencies, keeps its startup work to a couple of storage calls, and does not try to keep your service worker awake. That is a claim to verify rather than take from a vendor, and it is easy to verify, because the cost lands in four measurable places: bytes added to your background bundle, work done during service worker startup, writes to chrome.storage, and outbound network requests. This post shows how to measure each on your own extension.

TL;DR: Bundle size is the cost you pay on every service worker cold start, because background.js is evaluated again each time the worker starts. Startup costs a storage read and a write to each storage area. Events are batched rather than sent one request each. The question actually worth asking about any SDK is whether it uses chrome.alarms or a keepalive trick to stay awake, because that one changes when your extension runs and what its manifest asks for.

The four places analytics can slow down a Chrome extension

An extension analytics SDK is not a page tag: no script to fetch, no DOM to touch, no pixel to chain into. It is code running inside your background service worker, so its costs are the costs of anything else in that worker:

CostWhen it is paidBounded by
Bundle bytesEvery service worker cold start, when the bundle is evaluatedThe minified, gzipped size the SDK adds to your bundle
Startup workOnce per worker start, during init()One chrome.storage.local read, a chrome.storage.sync read only when no local id exists, and one write to each at the end of init
Storage writesDebounced after events are queued, and once after each send attemptThe persisted queue cap
NetworkWhen the batch fills, or on the flush intervalBatch size and flush interval, both configurable

Only the first is fixed once you ship, which is why it deserves the most attention.

No. 1Bundle bytes and cold start

In Manifest V3 your background service worker is torn down when it goes idle and started again on the next event it has a listener for, and every one of those starts evaluates your background.js again. So the number that matters is not the npm package's unpacked size, it is the compressed size the SDK adds to your bundle.

Measure it directly: build with and without the SDK, then compare:

# before
npm run build && gzip -9 -c dist/background.js | wc -c
# add the SDK, rebuild, run it again

For reference, the Moderok SDK's own minified bundle is 5.7 kB gzipped (5,671 bytes for dist/moderok.min.js, measured on 2 August 2026 with gzip -9 -c dist/moderok.min.js | wc -c), with zero runtime dependencies. Zero dependencies is the part to check on any candidate: a library that pulls in a UUID package, a polyfill and a date helper brings all of them into your bundle too, and its own source size tells you nothing about that. Run npm ls --all before you believe any published size figure, and re-measure after bundling, because tree shaking and your bundler settings change the result.

No. 2Startup work: what runs during init

The dangerous pattern in MV3 is not slowness, it is ordering. Chrome delivers events such as chrome.runtime.onInstalled to listeners registered in the first synchronous turn of the worker's startup, so a listener registered after an await can miss the event that woke the worker. An analytics SDK that does await chrome.storage.local.get(...) before registering anything can cost you an install event.

Moderok splits init() into two phases for exactly this reason. Phase one is synchronous: register the lifecycle listener and start the queue. Phase two is async: load storage, resolve the profile id, recover events pending from the last worker, then write the state back. Events tracked in between are held in memory as drafts and enqueued once phase two finishes. That window is short, but it is memory only, so a teardown inside it loses those drafts.

import { Moderok } from "@moderok/sdk";

// Runs first, synchronously, before any await in your own startup path.
Moderok.init({ appKey: "mk_your_app_key" });

chrome.runtime.onInstalled.addListener(() => {
  // your own listener, also registered synchronously
});

The rule when evaluating any SDK: call its init at the top of your background entry point, then confirm your own onInstalled and onMessage listeners still fire on a fresh install. If they do not, the SDK is doing async work before you get control. Calling track() before init() is handled too, and the init guide covers that case.

No. 3Storage writes

Persistence is not optional here. The worker can be torn down between the moment you call track() and the moment anything is sent, so an in-memory array alone loses events. What you want to check is write frequency, not the fact of writing.

Moderok debounces persistence by five seconds rather than writing per event, then writes again after each send attempt, and it caps what it keeps: 1000 events in memory, 500 persisted, and single events over 8 KiB skipped rather than queued. Those limits are published in the configuration reference. Note what the debounce implies: an event tracked in the seconds before a teardown may be neither sent nor persisted. Every batching SDK makes some version of that trade.

The caps matter because chrome.storage.local has a byte quota, and an unbounded analytics queue is a good way to find it. Check your own number from the service worker console:

chrome.storage.local.getBytesInUse(null, (bytes) => console.log(bytes));

If an analytics library is a meaningful share of that after a normal session, it is either not capping its queue or not draining it. Check chrome.storage.sync separately, since getBytesInUse on local does not report it. Moderok mirrors two small values there, the profile id and the last ping date, at the end of startup and again when a daily ping is accepted. With Chrome Sync on, that is what lets a profile id be recovered after a profile wipe.

No. 4Network requests

Events are batched, not sent one per call. Moderok's defaults are a batch of 20 events and a 30 second flush interval, both arguments to init(), so an extension that fires many events can raise the batch size and one that fires few can lower the interval or set it to 0 and flush manually. Requests go out with Content-Type: text/plain;charset=UTF-8, which keeps them out of the CORS preflight path, so each batch is one request rather than two.

Retryable failures back off rather than hammering: a failed batch is persisted and retried with exponential backoff, and a server-supplied Retry-After is honored. Your baseline volume also depends on what the SDK sends without being asked, which for Moderok is the five events listed under automatic events.

The question that actually matters: does it keep your worker awake?

An SDK that schedules chrome.alarms to flush, or runs a keepalive trick, changes when your worker runs, which changes your extension's memory and CPU profile. It also adds the alarms permission to your manifest.

Moderok's flush timer is an ordinary setInterval inside the worker, so it is cleared the moment the worker is torn down and never schedules a wake of its own, the way an alarm would. We covered that difference in chrome.alarms for periodic tasks in MV3. The consequence is worth being blunt about: with a 30 second interval and Chrome tearing the worker down after roughly 30 seconds of inactivity, an idle extension is often gone before the timer fires. Batches mostly go out when the batch fills or while your extension is awake anyway, and the rest is recovered from storage the next time something wakes the worker. If you would rather have events on a predictable schedule, that is a real tradeoff to weigh.

The four checks before you commit

Before committing to any extension analytics library, run these:

  1. Bundle diff. Build with and without, compare gzipped sizes, and check npm ls --all for a dependency tree.
  2. Startup ordering. Fresh install with the SDK initialized first, confirm your own onInstalled listener fires.
  3. Storage. getBytesInUse after a normal session, and confirm the queue is capped.
  4. Manifest. Anything beyond storage deserves an explanation. We went through the full list in what permissions Chrome extension analytics needs.

If those four come back clean, analytics is not what will slow your extension down. If you want an SDK built against these constraints from the start, read the getting started guide or see what the dashboard shows.