A Sane Approach to JavaScript Performance
JavaScript performance advice tends to come in two flavors. The first is trivia: "use const instead of var," as if variable declarations were why your page takes six seconds to become clickable. The second is terror: rewrite everything in a compiled framework, adopt islands architecture, hand-roll your own hydration. Both flavors miss the actual situation most site owners are in, which is this: you have a working site, it ships more JavaScript than it should, and you'd like it to feel faster without a rewrite.
This post is the middle path. It's organized by leverage — the things that matter most come first — and it assumes you'd rather delete code than tune it.
First, know what "slow" means for JavaScript
JavaScript hurts a page in three distinct ways, and they need different fixes:
- Download cost. Bytes over the network. This delays everything downstream, especially on cellular connections. Compression helps a lot here; a 300 KB bundle might be 90 KB over the wire.
- Parse and execute cost. Once downloaded, the browser has to compile and run the code. This is pure CPU time, and on a mid-range Android phone it can be 3–5× slower than on your development laptop. Compression does not help here at all — the browser executes the decompressed code.
- Long-task cost. Any script chunk that runs longer than 50 ms blocks the main thread, which means taps and clicks queue up behind it. This is what Google's INP metric (Interaction to Next Paint) measures, and it's the one users describe as "the site feels janky."
The practical implication: a large bundle that executes quickly in small pieces can feel better than a small bundle that runs one 800 ms task at startup. Measure both size and main-thread time.
Measure before you touch anything
Ten minutes of measurement saves days of misdirected effort. The workflow:
- Run PageSpeed Insights on your two or three most important pages. Look at the field data (real-user Chrome data) first, lab data second. If field INP is under 200 ms and LCP is under 2.5 s, your JavaScript is probably fine and you can stop reading — genuinely.
- Open Chrome DevTools → Performance, enable 4× CPU throttling, and record a page load. Long red-flagged tasks in the flame chart are your targets. Note which script file each one comes from.
- Open the Coverage tab (DevTools → More tools → Coverage) and reload. It shows what share of each file actually executed. It's common to find files that are largely unused on the page you're testing.
Write down the top three offenders by main-thread time. In my experience with WordPress sites, the list is usually: a page builder's frontend runtime, a third-party marketing script, and a slider or animation library loaded on pages that don't use it.
The highest-leverage move: ship less
No loading strategy beats not loading the code. In rough order of payoff:
Audit third-party scripts ruthlessly
Tag managers accumulate scripts the way junk drawers accumulate batteries. Pull up your tag manager (or your theme's header/footer injections) and ask, for each script: who looks at the data this produces, and when did they last look? Heatmap tools someone trialed long ago, a chat widget nobody staffs, two analytics platforms measuring the same thing — each one is 50–500 KB of JavaScript plus its own network handshakes. Removing one dead third-party script routinely does more for INP than a month of first-party optimization.
Load plugin assets only where they're used
The classic WordPress failure mode: a contact form plugin enqueues its scripts on every page, but you have one contact page. Same for sliders, tables, galleries, and booking widgets. Options, from least to most technical:
- Check the plugin's own settings — many now have a "load assets only on pages using the shortcode/block" toggle.
- Use an asset-management plugin to dequeue scripts per page or per post type.
- Dequeue in your child theme's
functions.phpwith a conditional: if the page isn't the contact page,wp_dequeue_script()the form assets.
Question the framework tax
If your theme or builder ships a large runtime to render what is essentially static content — text, images, a menu — that's parse-and-execute cost buying you nothing. You don't need to rewrite anything today, but when you next redesign, "how much JavaScript does this theme ship on a plain page?" belongs on the evaluation checklist. Under 100 KB compressed is a reasonable bar for a content site; some excellent themes ship under 30.
The second lever: load later and smarter
For the JavaScript that survives the audit, control when it runs.
defer is the default you want
A plain <script src> in the head blocks HTML parsing while it downloads and executes. Two attributes fix this:
defer— download in parallel, execute after the document is parsed, in order. This is the right choice for almost everything.async— download in parallel, execute the moment it arrives, order not guaranteed. Fine for independent scripts like analytics; risky for anything with dependencies.
Most WordPress optimization plugins can apply defer site-wide with an exclusion list. Start with everything deferred, then exclude scripts that break (typically inline scripts that expect jQuery to exist immediately).
Delay the truly non-essential
A more aggressive pattern: don't load certain scripts until the user interacts (first scroll, tap, or mouse move). Chat widgets, remarketing pixels, and video embeds are ideal candidates — nobody needs a chat bubble in the first 500 ms. Several caching plugins offer this as "delay JavaScript execution." The tradeoff is a brief delay when the widget is first needed; for a chat bubble that's fine, for your navigation menu it is not. Never delay scripts that power above-the-fold interactivity.
Facades for heavy embeds
A YouTube embed pulls in roughly half a megabyte of JavaScript before anyone presses play. The facade pattern loads a static thumbnail with a play button, and only swaps in the real player on click. Users who don't play the video — usually most of them — pay almost nothing. The same trick works for maps: a static map image that becomes interactive on click.
Fix long tasks, not milliseconds
If field data shows INP problems, hunt long tasks specifically:
- Break up big loops. Work that processes hundreds of items in one go can yield periodically —
await scheduler.yield()where supported, or asetTimeout(…, 0)chunking pattern — so input events can slip in between chunks. - Move heavy computation off the main thread. Web Workers are underused. Anything that doesn't touch the DOM — parsing, filtering large datasets, image processing — can run in a worker.
- Watch out for layout thrashing. Code that alternates reading layout properties (
offsetHeight) and writing styles in a loop forces repeated synchronous layouts. Batch reads, then batch writes.
One honest caveat: if the long tasks come from a third-party script or your page builder's core runtime, you often can't fix them directly. Your leverage is removal, delay, or replacement — see above.
What not to bother with
Some popular advice has a cost-to-benefit ratio that doesn't survive contact with reality:
- Micro-optimizing syntax. Loop style,
mapvsfor, arrow functions — irrelevant at page-load scale. The engine's JIT is smarter than the blog posts. - Minification heroics. Yes, minify — your build tool or optimization plugin already does. Chasing the last 2 KB with exotic compression settings is not where your afternoon goes.
- Preloading everything.
rel=preloadis a scalpel, not a firehose. Preload one or two genuinely critical resources (a hero image, a key font). Preloading ten scripts just reorders the congestion. - Switching frameworks for a content site. The rewrite costs six months; deleting your unused scripts costs an afternoon and usually wins.
A maintenance rhythm that keeps you fast
Performance regressions arrive one plugin at a time. A sustainable routine:
| Cadence | Task | Time |
|---|---|---|
| Monthly | PageSpeed Insights on top 3 pages; note field INP and LCP trend | 10 min |
| Quarterly | Third-party script audit: justify every entry in the tag manager | 30 min |
| On every new plugin | Check what it enqueues site-wide vs. per-page before activating in production | 10 min |
| Yearly | DevTools Performance recording with CPU throttling; compare against last year's notes | 1 hour |
The monthly check matters most. A number you look at regularly is a number that stays honest.
The short version
Measure with field data first; if users aren't feeling pain, don't manufacture a project. When they are: delete third-party scripts nobody uses, stop loading plugin assets on pages that don't need them, defer everything deferrable, delay the cosmetic, and facade the heavy embeds. Only then, if long tasks remain, get surgical with chunking and workers. JavaScript performance is mostly a discipline of subtraction — and subtraction, happily, is cheap.
Photo: Daniil Komov on Pexels