When an audit of a major news site reports a 49MB page, the instinct is to look at the Network tab in DevTools. Bytes transferred, number of requests, waterfall timing: these numbers are concrete and easy to read. They tell you what came over the wire.
They do not tell you what the browser’s CPU had to do with it afterward.
Five megabytes of JavaScript compressed is not five megabytes of work. It expands to 15-20MB of source text that the engine has to parse, convert to bytecode, and in some cases compile to native machine code, all before executing a single line of application logic. On a $200 Android phone with a mid-range Arm CPU, that work takes meaningfully longer than on the MacBook most developers use to build and audit sites. The gap is not proportional to clock speed. It is often 3 to 5 times larger because of differences in cache size, memory bandwidth, and thermal throttling behavior under sustained load.
What V8 Actually Does With a Script
When Chrome encounters a script tag or a dynamically injected script element, V8 runs it through a pipeline with several distinct phases, each of which has its own cost.
The first phase is parsing. V8 scans the source text and builds an Abstract Syntax Tree. This is CPU-bound work proportional to the size of the script, and it happens on the main thread for any script that was not loaded with <script src> (which can use a background streaming parser). For inline scripts, eval(), and new Function(), parsing is synchronous and single-threaded. A 300KB minified script, which might be 900KB uncompressed, can take 50-150ms to parse on desktop hardware.
The second phase is bytecode generation. V8’s Ignition interpreter converts the AST to a compact bytecode format. This is faster than parsing but still adds latency. The bytecode is what actually executes during initial page load for most functions.
The third phase, JIT compilation via TurboFan, is deferred. V8 does not compile all code to optimized machine instructions upfront; it profiles bytecode execution and only promotes hot code paths to TurboFan after gathering type feedback. For page-load scripts that run once and exit, TurboFan may never fire. For ongoing interaction handlers and polling loops added by analytics and session replay tools, it will.
There is also lazy parsing, which is V8’s optimization for functions that are defined but not immediately called. Rather than building a full AST for every function body, V8 does a quick pre-parse pass that checks syntax without building the full tree, deferring the full parse until the function is actually invoked. This helps, but it still visits every character of the source. You cannot skip code you have not read.
The Specific Scripts That Are Most Expensive
Not all third-party scripts carry equal parse cost. The worst categories by execution profile tend to be:
Header bidding libraries. Prebid.js is around 300KB minified with a full set of bidder adapters. It has to run before ad slots render, which means it executes during the critical period when the page is first becoming interactive. Beyond Prebid itself, each bidder adapter is additional code that initializes synchronously as part of the auction setup.
Session replay tools. Products like Hotjar and FullStory attach MutationObserver callbacks to the entire document and serialize DOM state on every change. They create persistent JavaScript objects representing the full document tree and send compressed snapshots to collection endpoints. This is ongoing CPU work throughout the session, not just a one-time initialization cost. A page with active DOM animation or real-time content updates keeps these tools continuously busy on the main thread.
A/B testing and personalization SDKs. Tools like Optimizely and VWO must run before the browser paints to prevent a flash of the unpersonalized content. They are deliberately placed in the <head> without async or defer attributes, which means they fully block HTML parsing until they complete. A test experiment that needs to redirect 5% of users to a variant page has to execute synchronously at the top of the document.
Tag management containers. Google Tag Manager’s container script is itself small, but it evaluates a JSON configuration blob that may reference dozens of vendor scripts, each loaded and evaluated as the container fires its triggers. The individual vendor scripts range from 20KB to several hundred KB, and they accumulate across the session as trigger conditions are met.
Reading a Performance Trace
The place to see this cost is Chrome DevTools’ Performance panel, not the Network tab. Record a trace during page load and look at the Main thread track. You will see alternating blocks of color representing parsing, script evaluation, layout, paint, and idle time.
Look specifically for Long Tasks, highlighted in red with a red corner triangle. A Long Task is any main thread block that exceeds 50ms. Each one represents a period during which the browser cannot respond to user input: clicks, taps, and scroll events are queued and ignored until the task completes.
On a news site with 50 third-party scripts, the main thread track during page load typically shows a dense sequence of Long Tasks stretching from document start to 8-15 seconds after initial paint. The page looks like it loaded to the user because pixels are on screen, but the browser is still processing script evaluation from tag manager injections and bidding library initialization.
Google’s Long Tasks API exposes this programmatically:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Long task:', entry.duration, 'ms', entry.attribution);
}
});
observer.observe({ entryTypes: ['longtask'] });
On a heavy news page, this callback fires dozens of times in the first 10 seconds. The attribution field points to which frame or script context owns each task, though third-party scripts from cross-origin frames often show limited attribution due to security isolation.
How INP Measures the Damage
Google’s Interaction to Next Paint metric, which replaced First Input Delay as a Core Web Vitals signal in March 2024, captures this problem at the user-visible level. INP measures the latency from user input to the next frame being painted, across all interactions during the session, and reports the worst-case value.
A page loaded with 5MB of active third-party JavaScript fails INP for a structural reason: long-running tasks on the main thread mean that when a user taps a navigation link or closes a cookie banner, that input event waits in the event queue behind whatever script evaluation task is currently running. If the browser is mid-way through evaluating a 200ms bidder adapter when the user taps, that tap has 200ms of forced latency before the browser even begins responding.
The INP threshold Google defines as “good” is under 200ms. Pages with heavy third-party JavaScript loads often register 500ms to over 1000ms on real devices, which Chrome’s field data (via CrUX) consistently shows correlates with higher bounce rates.
What Developers Can Actually Change
The root decisions around which third-party vendors to load are above the pay grade of most developers embedded in media organizations. But the execution timing of those scripts is often not.
Script evaluation deferral is the most direct lever. Any analytics or marketing script that does not need to affect initial render can be deferred with requestIdleCallback or loaded after the load event:
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
const script = document.createElement('script');
script.src = 'https://analytics.vendor.com/tracker.js';
document.head.appendChild(script);
});
}
This removes the script from the critical parse period without changing whether it fires. The data collection happens seconds later than it would have, which rarely matters for analytics purposes but makes the first 5 seconds of page load substantially cleaner.
Partytown takes a different approach by proxying third-party script execution into a Web Worker via a synchronous communication layer. Scripts that only need to read and write to global state, without requiring synchronous DOM manipulation, run entirely off the main thread. This is particularly effective for analytics pixels that just need to fire tracking calls.
Performance budgets in CI make regression visible before it ships. Lighthouse CI can fail a build when Total Blocking Time or JavaScript execution time crosses a threshold, which creates an organizational forcing function. A chart showing TBT growing 30ms per sprint becomes a conversation that abstract arguments about third-party scripts usually do not.
The Mobile Gap
Alex Russell has documented extensively that the CPU gap between developer hardware and the median device used to read the web is large and has not closed as fast as JavaScript payload sizes have grown. A 5MB script bundle that parses in 300ms on a MacBook M2 can take 1.2 to 1.5 seconds on a current mid-range Android device. Multiply that by the 15-20 distinct scripts that each require their own parse-and-execute cycle on a heavy news page and the numbers compound quickly.
The 49MB figure from the news audit is a transfer size. The CPU cost is something else entirely: a figure that depends on the device in the user’s hand, the temperature of the chip after 20 minutes of browsing, and how many other tabs are competing for memory. None of that shows up in a WebPageTest report run from a data center. It shows up in Core Web Vitals field data from CrUX, which aggregates measurements from real Chrome users on real hardware.
For news sites with global audiences, a large fraction of that real hardware is not the device in the developer’s desk drawer.