How to Handle Bundle Size in Inertia.js

2025-03-21 16:10:41 -0300

The Problem: Bundle Size in SPAs

One of the biggest challenges in building Single Page Applications (SPAs) is managing bundle size. As your application grows, the amount of JavaScript that needs to be loaded on the client increases, which can lead to slower initial page loads, negatively impacting performance and user experience.

By default, when using modern frontend frameworks like React, Vue, or Svelte, the entire JavaScript bundle is loaded when the application starts. This means that even pages or features that users may never access are included in the initial download. A common example of this issue is a rarely used feature pulling in a large third-party library, unnecessarily slowing down the entire app.

Example: Exporting PDFs with a Large Library

Imagine you have an Inertia.js application with an Export to PDF feature. This feature is only available on a specific page, but it relies on a large PDF rendering library (e.g., @react-pdf/renderer). If this library is included in the main bundle, all users will have to download it—even those who never use the export functionality.

Traditional Approach (Without Lazy Loading)

In a standard import setup, the PDF library is loaded immediately, regardless of whether the user ever accesses the page:

import React from 'react';

const MyPDFDocument = lazy(() => import('@/components/MyPDFDocument'));

const PDFViewPage = () => (
  <div>
    <h1>Export to PDF</h1>
    <MyPDFDocument/>
  </div>
);

export default PDFViewPage; 

This means that every user loads @react-pdf/renderer as soon as they visit the application, even if they never navigate to the export page. This can have a significant impact, especially if the library is large and comes with many dependencies. The following image shows the bundle size of @react-pdf/renderer.

Solution 1 - Lazy Loading Pages with Vite

The traditional approach in Inertia.js is to leverage Vite’s built-in code splitting by dynamically importing pages using import.meta.glob(). This method ensures that only the required page components are loaded when navigating between routes, significantly reducing the initial bundle size. However, this comes at a tradeoff—since pages are fetched asynchronously, it can introduce a slight delay when switching views, making the application feel **less snappy ** compared to eagerly loading all pages upfront.

// frontend/entrypoints/inertia.js
createInertiaApp({
  resolve: (name) => {
    const pages = import.meta.glob('../pages/**/*.jsx')
    return pages[`../pages/${name}.jsx`]()
  },
//...
})

Solution 2 - Lazy Loading with React Lazy

With lazy loading and code splitting, we can dynamically import the PDF library only when the user visits the export page. This ensures that users who don’t need the feature don’t download unnecessary code.

Dynamically Import the Component

We can use React’s built-in lazy and Suspense to load the PDF component only when needed:

import React, {lazy, Suspense} from 'react';

const MyPDFDocument = lazy(() => import('@/components/MyPDFDocument'));

const PDFViewPage = () => (
  <div>
    <h1>Export to PDF</h1>
    <Suspense fallback={<p>Loading PDF Renderer...</p>}>
      <MyPDFDocument/>
    </Suspense>
  </div>
);

export default PDFViewPage;

Now the component MyPDFDocument will only be loaded when the user visits the Export Page, reducing the initial bundle size.

Under the hood, when you use React.lazy(), Vite leverages dynamic imports (import()) to create separate chunks for each lazily loaded component. Then, when the component is rendered for the first time, the browser makes a network request to fetch the corresponding chunk asynchronously. This on-demand loading improves performance by ensuring that only the necessary code is loaded upfront, while additional pieces are fetched as needed.

Real Case Scenario

In a real-world scenario from one of my applications, we had an export PDF feature that was only available on one screen. Just like in the examples above, this meant the feature was unnecessarily included in the main bundle. By refactoring only six lines of code to use dynamic imports, we achieved a 75% reduction in the main bundle size, significantly improving performance.

Before bundle size

The changes we made

const ExamConfiguration = lazy(() => import("@/components/QuestionBank/Exams/ExamConfiguration"));
const PdfViewerModal = lazy(() => import("@/components/QuestionBank/Exams/PdfViewerModal"));
<Suspense fallback={<div>Loading...</div>}>
  <PdfViewerModal
    isOpen={showPdfPreview}
    examTitle={examTitle}
    questions={selectedQuestions}
    onClose={() => setShowPdfPreview(false)}
  />
</Suspense>

After bundle size

Conclusion

In summary, lazy loading with React and Vite can help reduce the initial bundle size and improve the user experience.

References