docs: clarify instrumentation-client polyfill loading and hydration timing (#95988)

The `instrumentation-client` Polyfills docs recommended a conditional
dynamic `import().then()` to load a polyfill. That import is
fire-and-forget: Next.js loads this file synchronously and does not
await it before hydration, so the polyfill can install after components
have already run (see #83905).

- In "Execution timing" mention that only synchronous top-level code
completes before hydration. Call out that a conditional `import()` here
is not awaited, and cross-reference `supported-browsers` for
load-on-demand strategies and the built-in baseline.

- In **`supported-browsers.mdx`**, "fix" the `Conditionally loading
polyfills` example to `await import()` instead of a floating `.then()`,
otherwise, a reader needs to put their code within the `.then` chain of
the polyfill load.

Closes:
https://linear.app/vercel/issue/DOC-6506/revisit-polyfills-with-instrumentation-client
This commit is contained in:
Joseph
2026-07-28 15:44:03 -04:00
committed by GitHub
parent 1412d0fe91
commit 7ec73949fc
2 changed files with 23 additions and 17 deletions
@@ -124,6 +124,8 @@ The `instrumentation-client.js` file executes at a specific point in the applica
This timing makes it ideal for setting up error tracking, analytics, and performance monitoring that needs to capture early application lifecycle events.
Only synchronous, top-level code is guaranteed to complete before hydration. Asynchronous work started here (a `Promise`, `import()`, or top-level `await`) is not awaited and may resolve after hydration has begun, so treat it as fire-and-forget. When something must be in place before your components run, use one of the synchronous patterns in [Polyfills](#polyfills).
## See also
`next.config.js` plugins (for example, wrappers like `withSentry`) can register their own client instrumentation module via the [`instrumentationClientInject`](/docs/app/api-reference/config/next-config-js/instrumentationClientInject) option. Injected modules run before this file, in array order, and may export the same router transition start hook. Application code should continue to use this file convention directly.
@@ -236,11 +238,23 @@ export function onRouterTransitionStart(url) {
### Polyfills
Load polyfills before application code runs. Use static imports for immediate loading and dynamic imports for conditional loading based on feature detection.
As noted in [Execution timing](#execution-timing), only synchronous, top-level code here runs before hydration, so a conditional `import()` or top-level `await` may resolve after hydration has already started.
```ts filename="instrumentation-client.ts" switcher
import './lib/polyfills'
To guarantee a polyfill is applied before your components run, statically import it and apply it synchronously after feature detection. Because the import is static, the polyfill ships to every visitor:
```ts filename="instrumentation-client.ts"
import ResizeObserverPolyfill from './lib/polyfills/resize-observer'
if (!window.ResizeObserver) {
window.ResizeObserver = ResizeObserverPolyfill
}
```
Avoid loading a polyfill with a conditional `import()` here. The import is fire-and-forget, so the polyfill may be applied after hydration has already begun, which can be too late for your components:
```ts filename="instrumentation-client.ts"
// Avoid: the dynamic import is fire-and-forget, so `ResizeObserver`
// may still be undefined when your components run.
if (!window.ResizeObserver) {
import('./lib/polyfills/resize-observer').then((mod) => {
window.ResizeObserver = mod.default
@@ -248,15 +262,9 @@ if (!window.ResizeObserver) {
}
```
```js filename="instrumentation-client.js" switcher
import './lib/polyfills'
For anything loaded on demand, prefer polyfilling in the code that uses the feature. See [Custom Polyfills](/docs/architecture/supported-browsers#custom-polyfills) for that and other strategies.
if (!window.ResizeObserver) {
import('./lib/polyfills/resize-observer').then((mod) => {
window.ResizeObserver = mod.default
})
}
```
Next.js already injects a baseline of [widely used polyfills](/docs/architecture/supported-browsers#polyfills) (such as `fetch`, `URL`, and `Object.assign`) for the browsers that need them, so you only need to add polyfills for features outside that baseline.
## Version history
+4 -6
View File
@@ -76,9 +76,8 @@ import { useCallback } from 'react'
export const useAnalytics = () => {
const tracker = useCallback(async (data: unknown) => {
if (!('structuredClone' in globalThis)) {
import('polyfills/structured-clone').then((mod) => {
globalThis.structuredClone = mod.default
})
const mod = await import('polyfills/structured-clone')
globalThis.structuredClone = mod.default
}
/* Do some work that uses structured clone */
@@ -94,9 +93,8 @@ import { useCallback } from 'react'
export const useAnalytics = () => {
const tracker = useCallback(async (data) => {
if (!('structuredClone' in globalThis)) {
import('polyfills/structured-clone').then((mod) => {
globalThis.structuredClone = mod.default
})
const mod = await import('polyfills/structured-clone')
globalThis.structuredClone = mod.default
}
/* Do some work that uses structured clone */