Neddar Islam
0%
⚛️Frontend Development
Featured

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.

Islam Neddar
3 min read
vite
micro-frontend
performance
react
bundle-optimization
umd
webpack
build-tools

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 or es?
  • 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 forces inlineDynamicImports: 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

ProblemSolution
No code splitting in UMDAccept single file output
Bundle size too largeExternalize big libraries (React, MapLibre)
Map library dependencyLoad from CDN (JS + CSS)
Runtime heavy componentsUse 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!

Share: