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