Strategies for Optimizing Your JavaScript Bundle for Better Web Performance

5 min read
📝 479 words
Strategies for Optimizing Your JavaScript Bundle for Better Web Performance

Strategies for Optimizing Your JavaScript Bundle

You can use the following techniques to optimize your JavaScript bundle:

Ideal Javascript Loading

  • To start, load only critical Javascript to get the app framework up and running
  • Next, load necessary Javascript modules for functionality
  • Conditionally import ESM modules only when they are needed (conditionally loading modules = significant perf benefits) when modules update, the browser only needs to download the new module, not all the Javascript on the site.

Javascript Optimization

  • Minify to reduce size
  • "Uglify" to improve code efficiency
  • Code split and use ESM modules when possible

Audit your js bundle size

I wrote this article on /automating-nextjs-bundle-size-monitoring to set up bundle size monitoring in your Next.js app.

Lazy Load Javascript

ESM Module and lazy loading support :

  • Webpack
  • Snowpack
  • Parcel
  • Rollup

Dynamic Import

dynamic import, it wont affect the initial page load size

import doSomethingCool from '../util.test'

async function onButtonClick() {
    const doSomethingCool = await import('../util/test'); // dynamic import, lazy load 

    doSomethingCool.default();
}

Using Intersection Observers

You might use intersection obsever to wait until a component comes in to the view and then load the javascript at that point

    const observer = new IntersectionObserver((entries) => {
        entries.forEach(async (entry) => {
            if (entry.isIntersecting) {
                const { helper } = await import ('more/crappy/js') // lazy load
            }
        })
    })

Create a custom hook to use intersection observer in react

In the example below, we have a LazyLoad component that uses the Intersection Observer API to load a heavy component only when it comes into the viewport.

import { useEffect, useRef, useState } from "react";
import { LazyLoad } from "../components/lazy-load";
import dynamic from "next/dynamic";
const HeavyComponent = dynamic(() => import("../components/heavy-component"), {
  ssr: false,
});
export default function Home() {
  return (
    <div className="min-h-screen flex flex-col items-center justify-center py-2">
      <h1 className="text-4xl font-bold mb-8">Lazy Load Example</h1>
      <div className="w-full max-w-2xl">
        <p className="mb-4">
          Scroll down to load the heavy component...
        </p>
        <div style={{ height: "800px" }}></div> {/* Spacer to enable scrolling */}
        <LazyLoad>
          <HeavyComponent />
        </LazyLoad>
      </div>
    </div>
  );
}

LazyLoad Component

"use client";
import { useEffect, useRef, useState } from "react";

interface LazyLoadProps {
  children: React.ReactNode;
  className?: string;
}

export const LazyLoad = ({ children, className }: LazyLoadProps) => {
  const [isVisible, setIsVisible] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      {
        rootMargin: "50px",
      }
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => {
      observer.disconnect();
    };
  }, []);

  return (
    <div ref={ref} className={className}>
      {isVisible ? (
        children
      ) : (
        <div className="animate-pulse flex space-y-4">
          <div className="flex-1 space-y-4 py-1">
            <div className="h-4 bg-gray-200 rounded w-3/4"></div>
            <div className="space-y-2">
              <div className="h-4 bg-gray-200 rounded"></div>
              <div className="h-4 bg-gray-200 rounded w-5/6"></div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};