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

> ### 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**

```bash
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](https://www.npmjs.com/package/@next/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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764851994350/68a753ee-8b32-4e4e-9804-d9d169457f06.png align="center")

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764851637542/727f4933-315e-4da6-ae31-df3f8ff6b56a.png align="center")

**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.

```typescript
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 }
}
```

```typescript
// 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**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764852097826/8fb14759-42f2-408b-bec4-cb38de881957.png align="center")

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
