Cloudflare Rocket Loader Breaking Next.js INP Scores Fix

Cloudflare Rocket Loader tanking INP on a Next.js site? Here is why it delays React hydration on mobile and the page rule that restores interactivity.
PerformanceCore Web VitalsCloudflare
May 24, 20266 min read1189 words

The Problem

A client's marketing site sailed through every Core Web Vitals check on the staging environment. LCP 1.4s on a slow 4G profile, CLS 0.02, INP 110ms on the mobile test runs. We shipped to production behind Cloudflare. Twenty-four hours later the CrUX-backed field data in PageSpeed Insights showed mobile INP at the 75th percentile climbing past 500ms. Desktop was fine. Lab runs from Lighthouse and WebPageTest still showed the staging numbers. Real Chrome on real phones told a different story.

The site is a fairly standard Next.js 16 App Router build with a few client islands — a mega menu, a newsletter form, a sticky CTA bar. None of those had changed in the deploy. The only thing that had changed was the DNS pointing at Cloudflare instead of straight at Vercel.

When I opened the live page on a real iPhone and looked at the document source, every <script> tag — including the Next.js bootstrap scripts in the <head> — had been rewritten to type="text/rocketscript". That was the smoking gun.

Why It Happens

Cloudflare's Rocket Loader is an old optimisation that defers and bundles JavaScript execution to improve perceived paint time. The way it does that is to rewrite every <script> tag in the HTML response to a custom MIME type that the browser does not natively execute. It then injects a small Rocket Loader runtime that, after the document is interactive, walks the rewritten tags in order and executes them.

That works tolerably well on a static jQuery-era page where script execution is independent of layout. It is hostile to a modern React app for two reasons.

First, the Next.js client bootstrap script and the React runtime get rewritten alongside everything else. The browser parses the HTML, sees type="text/rocketscript" on the bundle that contains React, and refuses to execute it. The page renders the static HTML shell, the user taps a button, and nothing happens until the Rocket Loader runtime decides to schedule the React bundle. On a fast desktop that gap is invisible. On a mid-range Android phone over 4G the schedule can land 800-1500ms after the user's first tap, which is exactly the window the Interaction to Next Paint metric measures.

Second, the rewrite changes the order in which next/script strategies actually execute. Anything you marked beforeInteractive is no longer guaranteed to run before hydration, because Rocket Loader does not respect React's hydration boundary — it respects its own internal queue. So a third-party tag you needed early (a consent manager, a polyfill) lands late, and a hydration-blocking script you intended to load afterInteractive can land before hydration. The net effect is interactive UI elements that look ready but ignore taps.

Lab tools miss this because Lighthouse and WebPageTest typically run from Vercel-friendly origins or with a User-Agent that Cloudflare excludes from Rocket Loader. Real mobile Chrome on a residential IP gets the full treatment, which is why CrUX shows the regression and your npm run lighthouse does not. Cloudflare's own documentation now says explicitly that Rocket Loader is not recommended for sites built with modern JavaScript frameworks. That note was added quietly and is easy to miss when an agency turned the feature on years ago and nobody owned it.

The Fix

Turn Rocket Loader off. Not selectively, not for some routes — globally for the zone, or via a page rule for the entire hostname. The Next.js client bootstrap cannot tolerate the rewrite under any configuration.

Step 1 — Disable Rocket Loader at the zone level. In the Cloudflare dashboard go to Speed → Optimization → Content Optimization. Scroll to "Rocket Loader" and switch it off. That is the cleanest fix and the one I always reach for first. If you have legitimate legacy pages on the same zone that need it (you almost certainly do not), use a Configuration Rule instead.

Step 2 — Add a Configuration Rule as a safety net. Even after the global toggle, a future admin can re-enable it. Pin the behaviour for the relevant hostname so this does not regress:

When incoming requests match:
  Hostname equals www.example.com

Then the settings are:
  Rocket Loader: Off
  Auto Minify (JS): Off
  Email Obfuscation: Off

Auto Minify and Email Obfuscation also rewrite Next.js HTML in ways that can break hydration or strip whitespace inside <pre> blocks. Turning all three off as a group is the standard hardening I apply to every Cloudflare zone in front of a React or Next.js app.

Step 3 — Per-tag opt-out, only if the global toggle is impossible. If a client refuses to disable Rocket Loader site-wide, you can mark individual <script> tags to be skipped. Next.js exposes a data-cfasync prop through next/script:

import Script from 'next/script'

export function Analytics() {
  return (
    <Script
      src="/analytics.js"
      strategy="afterInteractive"
      data-cfasync="false"
    />
  )
}

That works for tags you control. It does not work for the Next.js bootstrap scripts, which you do not render directly. There is no first-class way to add data-cfasync="false" to those tags, so the per-tag escape hatch is only useful for third-party additions, not for the framework itself. Which means: turning Rocket Loader off is the only complete fix.

Step 4 — Verify with real-device data. Lab tools will not show the change. Open the live page on an actual phone, run the Chrome DevTools remote inspector, and confirm the script tags in the response now have type="module" or no type attribute, not text/rocketscript. Then watch the CrUX dashboard in PageSpeed Insights over the next 24-48 hours. The 75th percentile INP should drop into the green band within a day on a moderate-traffic site.

curl -s -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)" \
  https://www.example.com | grep -c "rocketscript"

The output should be 0. If it is not, the Configuration Rule did not match — check the hostname pattern.

The Lesson

Rocket Loader rewrites every <script> tag in the HTML response, including the Next.js bootstrap, which delays hydration on the exact devices that the INP metric measures. Lab tools miss it because they do not get the rewrite. CrUX field data catches it within a day. Turn Rocket Loader, JS auto-minify, and email obfuscation off on any Cloudflare zone fronting a React or Next.js application.

If your Core Web Vitals dashboard turned red after a CDN change and you cannot reproduce it locally, that is the diagnosis-and-fix work I do most weeks. See my services. For another INP regression that lab tools miss, read INP regression from Sentry Session Replay on mobile.

If your INP scores went red after a CDN change, I can sort it.

Back to blogStart a project