Cloudflare Rocket Loader Breaking Next.js INP and Hydration

Cloudflare Rocket Loader tanking your Next.js INP and triggering hydration warnings after enable? Here is the page rule and meta tag fix that restores both.
Cloudflare Rocket Loader Breaking Next.js INP and Hydration
PerformanceNext.jsCore Web Vitals
June 13, 20266 min read1182 words

The Problem

A client turned on Rocket Loader inside Cloudflare last week because their SEO consultant told them it would help Core Web Vitals. The opposite happened. Within two days the Next.js 16 frontend's INP regressed from 180 ms to 620 ms in CrUX, the Search Console "Poor INP" bucket grew by 41% on mobile, and the production logs filled with hydration warnings that had never fired before:

Hydration failed because the server rendered HTML didn't match the client.
As a result this tree will be regenerated on the client.

  - Expected server HTML to contain a matching <script> in <head>.
  +  <script async data-cf-settings="..." src="https://ajax.cloudflare.com/cdn-cgi/scripts/...">

The page still rendered. Users could still click around. But every interaction logged an INP event in the high hundreds of milliseconds, and the React tree was being thrown away and re-rendered on the client. The vitals report on Vercel matched the CrUX regression within 24 hours.

Three things pointed at Rocket Loader before I even opened DevTools. The regression started the same day it was enabled. Disabling it on a single test page made the warning go away. And the <script> tag in the warning had data-cf-settings on it, which is Rocket Loader's signature.

Why It Happens

Rocket Loader is a Cloudflare edge transformation. When enabled, it rewrites the HTML response before delivery: every <script> tag in the document is replaced with a placeholder, and a single Rocket Loader bootstrap script is injected to load and execute the originals asynchronously. The goal is to keep render-blocking scripts off the critical path.

The model assumes the original HTML is static. Next.js HTML is not static in the way Rocket Loader expects. The App Router server-renders an HTML payload that includes inline <script> tags carrying the React Server Component (RSC) wire format, plus the client manifest, plus per-route chunk hints. React 19.2 reads those scripts during hydration to reconstruct the component tree on the client. Rocket Loader rewrites them. By the time React tries to hydrate, the scripts it expected are placeholders, the scripts it sees are Rocket Loader bootstrap stubs, and the server/client trees do not match. React falls back to a full client render, which is the INP collapse.

The hydration warning is the proximate cause. The INP regression has a second cause too: Rocket Loader's bootstrap script is itself a long task, often 400–700 ms on a mid-tier Android. It runs on the main thread before React's hydration starts. Every interaction during that window is queued, and the first interaction that lands after hydration carries the cost of both jobs.

Cloudflare's Rocket Loader docs acknowledge that JavaScript frameworks "may experience issues" but do not single out the RSC payload case.

The Fix

Two layers. Disable Rocket Loader where it harms you, then make sure it stays off if someone re-enables it at the zone level.

Layer 1: Mark scripts as off-limits to Rocket Loader. You can opt out per-script with a data-cfasync="false" attribute. For Next.js you do not want to do that per script, because the RSC inline scripts are generated by React, not by your code. You want to opt the whole document out by signalling intent in the response headers, which Cloudflare honours before the HTML transform runs:

// next.config.ts
const nextConfig = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          { key: 'Cache-Control', value: 'public, max-age=0, must-revalidate' },
          { key: 'x-no-rocket-loader', value: '1' },
        ],
      },
    ]
  },
}
export default nextConfig

Then create a Cloudflare Page Rule (or a Configuration Rule under Rules → Configuration Rules) that disables Rocket Loader when that header is present, or more simply, on the production hostname:

If incoming requests match:
  Hostname equals example.com
Then:
  Rocket Loader: Off

Configuration Rules execute before the HTML transform, so the response Cloudflare delivers contains the original <script> tags with the RSC payload intact. React hydrates against an unmodified tree, and the warning disappears on the next deploy.

Layer 2: Catch the re-enable. Rocket Loader is a zone-level toggle that any teammate with Cloudflare access can flip back on, and the symptoms take 24 hours to show up in CrUX. Add a deploy-time check that asserts Rocket Loader is off on the production hostname:

#!/usr/bin/env bash
RESPONSE=$(curl -sI https://example.com/)
if echo "$RESPONSE" | grep -qi 'cf-ray'; then
  if curl -s https://example.com/ | grep -q 'data-cf-settings'; then
    echo "FAIL: Rocket Loader is active on production"
    exit 1
  fi
fi
echo "OK: Rocket Loader is not transforming production HTML"

Wire that into the post-deploy step of your CI. If Rocket Loader gets switched back on, the next deploy fails fast with a clear error, instead of you finding out from a CrUX update a week later.

Layer 3: Recover the INP signal in the field. Rocket Loader long tasks stay in the field data for 28 days, which is how long CrUX averages over. The vitals report will lag the fix. You can speed up confidence by watching the live signal from the Web Vitals attribution build:

import { onINP } from 'web-vitals/attribution'

onINP((metric) => {
  const longest = metric.attribution.longestScript ?? 'unknown'
  navigator.sendBeacon('/api/vitals', JSON.stringify({
    name: 'INP',
    value: metric.value,
    longestScript: longest,
    path: location.pathname,
  }))
})

After the fix, longestScript should stop reporting Cloudflare URLs, and your p75 INP should land back near the pre-Rocket-Loader baseline within 48 hours. CrUX will catch up over the following four weeks.

Verify the hydration warning is gone. Production-mode next build && next start against the live Cloudflare zone, then check the console:

next build && next start
# In another terminal:
curl -s https://example.com/ | grep -c 'data-cf-settings'
# Should print 0

If the count is non-zero, the Configuration Rule did not match — usually because the hostname filter is too narrow.

The Lesson

Rocket Loader rewrites every <script> tag in the document, including the inline scripts React 19.2 uses to hydrate the App Router tree. The rewrite breaks hydration, falls back to a full client render, and adds a 400–700 ms long task to the main thread. Disable it for the Next.js hostname with a Configuration Rule, assert it stays off in CI, and watch the INP attribution signal recover before CrUX catches up.

If your Core Web Vitals tanked after a CDN setting changed and the team cannot reproduce it locally, that is the kind of work I get paid for. See my services. For a related INP regression caused by a third-party tag, read INP regression from GTM third-party tags.

Vitals regressed after a CDN setting flipped? Get it diagnosed.

Back to blogStart a project