descricao: “Learn the exact fix for the ResizeObserver loop limit exceeded error caused by Vite’s experimental prebundle in SvelteKit 2.0 projects.”
I ran into a nasty ResizeObserver loop limit exceeded error the moment I turned on Vite’s experimental prebundle flag in a brand‑new SvelteKit 2.0 app. The console exploded, the UI flickered, and my dev server refused to hot‑reload. After a few hours of digging through Vite’s changelog, SvelteKit’s adapter code, and the browser’s ResizeObserver spec, I finally landed on a deterministic fix that works for any production‑grade SvelteKit project.
In this post I’ll walk you through the exact conditions that trigger the error, show the broken setup, and then give you a step‑by‑step patch that eliminates the warning without sacrificing the performance gains of Vite’s prebundle. All the code is runnable, and I’ll sprinkle a couple of “Pro Tip” nuggets along the way.
Why does enabling Vite’s experimental prebundle break ResizeObserver in SvelteKit 2.0?
When you add experimental.prebundle: true to vite.config.js, Vite tells the underlying Rollup pipeline to bundle all third‑party modules before they hit the dev server. The idea is to reduce the number of HTTP requests and speed up cold starts. It works great for most libraries, but there’s a hidden interaction with SvelteKit’s built‑in layout handling:
- SvelteKit renders the root layout inside a
<div id=\"svelte\">element. During the first render it registers aResizeObserverto keep the layout height in sync with the viewport. - Vite’s prebundle rewrites the import graph for the
resize-observer-polyfill(or the nativeResizeObservershim that some UI libraries bring). The polyfill ends up being evaluated twice – once in the prebundled chunk and once again when the app’s own bundle runs. - The duplicate observers start observing the same DOM node, each trying to report size changes. The browser detects a feedback loop: an observer triggers a layout, which triggers the observer again, and after a few iterations the spec forces a
ResizeObserver loop limit exceedederror.
The error is not just a warning; Vite’s hot‑module‑replacement (HMR) aborts, and you end up with a frozen dev server.
How can I reproduce the error in a minimal SvelteKit 2.0 project?
Below is the exact scaffolding I used. If you follow these steps you’ll see the error within seconds.
# 1. Create a fresh SvelteKit project (npm 9+, node 20)
npm create svelte@latest my‑app
cd my‑app
npm install
# 2. Enable Vite’s experimental prebundle
cat <<'EOF' > vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
/** @type {import('vite').UserConfig} */
export default {
plugins: [sveltekit()],
experimental: {
prebundle: true // <-- this flag triggers the bug
}
};
EOF
# 3. Add a component that uses ResizeObserver directly (many UI libs do this internally)
mkdir -p src/lib && cat <<'EOF' > src/lib/ResizableBox.svelte
<script>
import { onMount, onDestroy } from 'svelte';
let box;
let ro;
onMount(() => {
ro = new ResizeObserver(entries => {
for (const entry of entries) {
console.log('size', entry.contentRect.width, entry.contentRect.height);
}
});
ro.observe(box);
});
onDestroy(() => ro.disconnect());
</script>
<div bind:this={box} style="width: 100%; height: 200px; background: #eef;">
Resize me!
</div>
EOF
# 4. Use the component in src/routes/+page.svelte
cat <<'EOF' > src/routes/+page.svelte
<script>
import ResizableBox from '$lib/ResizableBox.svelte';
</script>
<ResizableBox />
EOF
# 5. Run the dev server
npm run dev
When the page loads you’ll see in the browser console:
ResizeObserver loop limit exceeded
And the Vite dev overlay will flash “Error while evaluating module …”.
What is the simplest way to stop the ResizeObserver loop without disabling prebundle?
The root cause is the duplicate import of the ResizeObserver polyfill. The fix is to ensure the polyfill is externalized from the prebundle step so that Vite only includes the native implementation once.
Step‑by‑step fix
- Create a Vite alias that points to the native implementation and mark it as external for the prebundle.
- Add a tiny shim that re‑exports the native
ResizeObserver(or the polyfill if the browser lacks it). - Tell Vite not to prebundle that shim using the
optimizeDeps.excludeoption.
Here’s the final vite.config.js:
import { sveltekit } from '@sveltejs/kit/vite';
import path from 'node:path';
/** @type {import('vite').UserConfig} */
export default {
plugins: [sveltekit()],
resolve: {
alias: {
// Resolve any import of "resize-observer-polyfill" to our shim
'resize-observer-polyfill': path.resolve('src/lib/ResizeObserverShim.js')
}
},
optimizeDeps: {
// Prevent Vite from pre‑bundling the shim – it will be evaluated only once
exclude: ['resize-observer-polyfill']
},
experimental: {
prebundle: true // keep the performance boost
}
};
And the shim (src/lib/ResizeObserverShim.js):
// src/lib/ResizeObserverShim.js
// This file is deliberately tiny – it either exports the native API or falls back to the polyfill.
let ResizeObserverConstructor;
if (typeof window !== 'undefined' && window.ResizeObserver) {
ResizeObserverConstructor = window.ResizeObserver;
} else {
// Dynamically import the polyfill only when needed – this import will not be pre‑bundled.
// eslint-disable-next-line @typescript-eslint/no-var-requires
ResizeObserverConstructor = require('resize-observer-polyfill');
}
export default ResizeObserverConstructor;
Now, any component that does new ResizeObserver(...) will resolve to this shim, and Vite will leave the shim out of the prebundle. The duplicate observer problem disappears, and the dev server runs happily.
Pro Tip: Keep the shim in
src/lib(or any folder that Vite treats as source) so that hot‑module‑replacement still works when you edit the shim. If you move it tonode_modules, Vite will treat it as an external dependency and you’ll lose HMR for that file.
How does the fix affect production builds?
In production, SvelteKit runs a full Rollup bundle (no Vite dev server). The optimizeDeps.exclude flag only applies to the dev server, so the shim is bundled normally. Because the shim merely re‑exports the native ResizeObserver (or the polyfill), the final bundle size is unchanged – you still get the polyfill only when the runtime environment needs it.
If you are targeting evergreen browsers that all support ResizeObserver, the shim resolves to the native constructor and the polyfill never gets pulled in, keeping the bundle lean.
What if I’m already using a UI library that bundles its own ResizeObserver polyfill?
Many component libraries (e.g., Carbon, Radix, Headless UI) ship their own copy of resize-observer-polyfill. The same duplicate‑observer problem can arise even if you never import the polyfill directly.
The robust approach is to force all imports of the polyfill to resolve to the same shim. You can do this by adding a wildcard alias:
resolve: {
alias: {
// Any path that ends with "resize-observer-polyfill" resolves to our shim
/^.*\/resize-observer-polyfill$/: path.resolve('src/lib/ResizeObserverShim.js')
}
},
optimizeDeps: {
// Exclude the polyfill from prebundle regardless of how it’s imported
exclude: ['resize-observer-polyfill']
},
This pattern works because Vite’s alias resolution runs before the dependency graph is built, so even deep imports inside a library get redirected.
Pro Tip: If you’re using TypeScript, add a declaration file (
src/lib/ResizeObserverShim.d.ts) that declares the default export astypeof ResizeObserverto keep the compiler happy.
How can I verify that the error is truly gone?
- Open Chrome DevTools → Console.
- Reload the page after applying the shim.
- You should no longer see ResizeObserver loop limit exceeded.
- In the Network tab, filter by JS and confirm that the polyfill is not being downloaded twice (you’ll see a single request for
ResizeObserverShim.js). - Run the Vite dev server and make a change in any component; HMR should work without the overlay flashing an error.
If the warning still appears, double‑check that all imports of resize-observer-polyfill are being redirected. Use the following command to search the lockfile:
grep -R "resize-observer-polyfill" node_modules | wc -l
If the count is greater than zero, you likely have a stray copy that isn’t being aliased. Add another pattern to the alias until the count drops to zero.
FAQ
How do I know if Vite’s prebundle is actually enabled in my project?
Vite prints a log line prebundle enabled when you start npm run dev. You can also inspect vite.config.js – the experimental.prebundle flag must be true.
Does disabling the prebundle completely solve the ResizeObserver error?
Yes, turning off experimental.prebundle removes the duplicate import, but you lose the performance boost for large dependency trees. The shim approach gives you the best of both worlds.
Will this fix work with SvelteKit 1.x?
SvelteKit 1.x uses Vite 2, which does not have the experimental.prebundle flag. The error is specific to the new Vite 4 prebundle implementation, so you won’t see it in 1.x projects.
Is there a way to automate the alias for any future polyfills that cause similar loops?
You can create a small helper function in vite.config.js that maps a list of problematic packages to a common shim folder. Example:
function aliasPolyfills(packages) {
const map = {};
for (const pkg of packages) {
map[`^.*\/${pkg}$`] = path.resolve('src/lib/PolyfillShims', `${pkg}.js`);
}
return map;
}
export default {
resolve: { alias: aliasPolyfills(['resize-observer-polyfill', 'some-other-polyfill']) },
// …
};
Conclusion
The ResizeObserver loop limit exceeded error after enabling Vite’s experimental prebundle in SvelteKit 2.0 isn’t a bug in SvelteKit itself – it’s a side‑effect of the prebundle pulling in the same ResizeObserver implementation twice. By externalizing the polyfill through a tiny shim and telling Vite not to prebundle that shim, you preserve the speed gains of prebundle while eliminating the duplicate‑observer feedback loop.
From an engineering perspective the key takeaway is: when you enable aggressive bundling optimizations, audit any global browser APIs that libraries may polyfill. A single duplicated observer can bring the whole dev experience to a halt, but a few lines of alias configuration restore stability.
Pro Tip: Keep the shim logic isolated and version‑agnostic. If browsers add native support for a new API, the shim will automatically fallback to the native implementation without any code change.
If you found this deep dive useful, stay tuned to SpiritCode for more real‑world fixes and performance tricks.

