Optimizing Micro-frontend Widget Built with Vite for Script Tag Loading
Learn how to optimize Vite bundles for classic script tag loading, externalize heavy libraries, and reduce bundle size from 1MB to 60KB.
Introduction
Recently, while working on a project, I had to build a micro-frontend widget (a "Locate Seller Modal") using Vite, React, and Vanilla Extract. The widget needed to be loaded into an existing website via a simple <script>
tag (not type="module").
Sounds simple? 🤔 Actually, it led me into an interesting real-world problem:
- Vite output format? Should it be
umd
ores
? - How to optimize bundle size when code splitting is not allowed?
- How to externalize heavy libraries like
maplibre-gl
?
This blog will walk you through what I learned, the challenges, and the clean final solution. 📈
Problem: Using <script>
Tags Without type="module"
The website loads the widget with a simple classic script:
<script src="https://dev.website.fr/static/fragment-contact-modals/locate-seller-modal-XXXX.js"></script>
Because it's a classic <script>
(not type="module"
), it forces us to output our bundle in UMD format (Universal Module Definition).
Key limitation:
UMD format does NOT allow code splitting or multiple chunks.
First Challenge: No Code Splitting Allowed in UMD
When trying to use Vite's manualChunks
or dynamic imports (React.lazy
), I encountered:
Invalid value for option "output.manualChunks" - this option is not supported for "output.inlineDynamicImports".
Why?
UMD
forcesinlineDynamicImports: true
- Which means all dynamic imports must be inlined into one big file
Lesson: If you build UMD, accept that you will have a single bundle file.
Second Challenge: Bundle Size Explosion
Without code splitting, everything was being bundled together:
- React
- React-DOM
- Axios
- MapLibre-GL (a heavy WebGL map library)
Result: The final locate-seller-modal.js was almost 1MB (uncompressed).
This is unacceptable for web performance.
Solution 1: Externalize Heavy Libraries
To shrink the bundle, I externalized the following libraries:
external: [
'react',
'react-dom',
'react/jsx-runtime',
'axios',
'maplibre-gl',
],
And told Vite/Rollup how they exist globally:
globals: {
react: 'React',
'react-dom': 'ReactDOM',
'react/jsx-runtime': 'jsxRuntime',
'maplibre-gl': 'maplibregl',
axios: 'axios',
},
CDN Loading
I then loaded these libraries separately via CDN in the host page:
<link href="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" defer></script>
<script src="https://dev.website.fr/static/fragment-contact-modals/locate-seller-modal-XXXX.js" defer></script>
This reduced my own bundle (locate-seller-modal.js
) from nearly 1MB to about 60KB.
Solution 2: React.lazy() + Suspense (Optional for UX)
Even if we can't split files physically, I still used React.lazy()
internally to lazy-evaluate heavy components (like Map).
const LazyLocateSellerModal = React.lazy(() => import('./LocateSellerModal'))
root.render(
<Suspense fallback={<div>Loading...</div>}>
<LazyLocateSellerModal serverData={serverData} />
</Suspense>
)
This approach does not split files, but still:
- Improves app load responsiveness (even inside one file)
- Shows fallback UI while heavy components initialize
Conclusion: Key Lessons
Problem | Solution |
---|---|
No code splitting in UMD | Accept single file output |
Bundle size too large | Externalize big libraries (React, MapLibre) |
Map library dependency | Load from CDN (JS + CSS) |
Runtime heavy components | Use React.lazy + Suspense |
In short: When building micro-frontends with Vite for
<script>
loading (classic), focus on externalizing, lazy-evaluation, and smart loading — not on chunk splitting.
And yes — even if you can't split chunks in UMD, you can still make your app ✨ lightning fast ✨ with these tricks.
Bonus: Future Evolution
When your platform allows <script type="module">
later, you can switch to:
format: 'es'
- Full code splitting
- Native browser module loading
... but until then, mastering UMD optimization is a true superpower. 🚀
Thanks for reading!