Skip to main content

Command Palette

Search for a command to run...

What I Learned Optimizing Our Next.js Bundle Size (and How You Can Do It Too)

Updated
3 min read

During one of our recent feature releases, we noticed something concerning: our initial page load bundle size had increased. As the application grows, this kind of creeping increase can negatively impact performance — especially for users on slower networks. So, we decided it was time to investigate and optimize.

Step 1: Checking the Build Output

yarn build

This allowed me to see the bundle breakdown for each route in our Next.js application. While the output gave some high-level numbers, I wanted more visibility into which packages were increasing the size.

Step 2: Analyzing With Bundle Analyzer

To dig deeper, I used the Next.js Bundle Analyzer plugin. This tool visualizes everything included in the client bundle. Once the build completed, the analyzer made one issue very obvious:

The first load js shared by all is 1.16MB before lazy loading as you can see below

👉 The emoji-mart package we introduced for our new feature was taking up ~100 KB gzipped just on the initial load.

Step 4: Dynamic Import to the Rescue

To fix this, I removed the default static import from the main component and created a custom React hook to dynamically import the library when the user interacts with the feature.

This approach gives us code-splitting, meaning the emoji picker is bundled separately and only downloaded when required.
Created a custom hook to load emoji-mart library on user click.

export const useEmojiPicker = () => {
  const [emojiDataLoaded, setEmojiDataLoaded] = useState(false)
  const [emojiData, setEmojiData] = useState<any>(null)

  const loadEmojiData = async () => {
    if (!emojiDataLoaded) {
      try {
        const data = await import('@emoji-mart/data')
        setEmojiData(data.default)
        setEmojiDataLoaded(true)
      } catch (error) {
        console.error('Failed to load emoji data:', error)
      }
    }
  }

  return { emojiData, emojiDataLoaded, loadEmojiData }
}
// Parent Component
const { emojiDataLoaded, emojiData, loadEmojiData } = useEmojiPicker()

// OnClickHandler
const onClickHandler = async () {
    // Load emoji data on first picker open
    if (!emojiDataLoaded) {
      await loadEmojiData()
    }
}

By doing this during build time, the webpack will split the emoji-mart package into separate chunks and it is loaded when needed instead of first page load.

Step 5: Reduced bundle size

After applying the change, I ran yarn build again.

Result: 🎉

Our first page load bundle size dropped from 1.16 MB → 1.05 MB.

It may not sound huge, but in performance work:

Small optimizations compound — and early wins help guide future improvements.

Conclusion:
This was just the first step in optimizing our growing codebase. Moving forward, we’ll continue

  • Auditing unused dependencies

  • Applying lazy loading where possible

  • Monitoring bundle size for every new feature

  • Automating alerts when thresholds are exceeded