We Cut Our React Bundle by 40% by Rethinking How We Handle Translations
A story about scaling to 20+ languages without slowing anything down
The Hook
Every time we added a new language to our React app, the build got heavier. Startup got slower. Memory usage crept up. And somewhere along the way, “let’s expand to a new market” quietly became an engineering risk instead of a product win.
We fixed that. Not with a plugin, not with a shortcut — with an architecture change. The result: roughly a 40% reduction in production bundle size, faster startup, better caching, and a path to supporting 20+ languages (LTR and RTL) without touching the core app again.
Here’s how we got there.
The Problem
Our translations were doing exactly what most teams’ translations do in the early days: living directly inside the codebase, bundled with everything else.
That worked fine when we supported two languages. It stopped working around language number five.
- Every language file shipped inside the JS bundle, whether a user needed it or not.
- Adding a language meant a bigger app for every user, everywhere, regardless of what they actually spoke.
- Updating a single translation string meant a full rebuild and redeploy — for a copy change.
- Strings were scattered and hardcoded across components, which made consistency and QA a constant chase.
- Startup time and memory usage both crept upward as our language list grew.
None of this was a bug. It was the natural ceiling of an architecture that treated translation content the same way it treated application logic — as something that lives and ships with the code. The problem is, content and code change at completely different speeds, and once you’re serving a global user base, that mismatch starts costing you real performance.
The Solution
We rebuilt localization from the ground up, with one guiding principle: translations should be content, not code.
A clean, key-based translation layer. We implemented i18next and react-i18next across the app, replacing hardcoded strings with the useTranslation() hook. Every label, message, tooltip, validation error, and notification now resolves dynamically through translation resources instead of living inline in components.
Only load what the user actually needs. Instead of bundling every language into the app, we configured i18next to fetch the required language JSON over HTTP at startup — and only that one. If a user is browsing in French, we don’t ship them Japanese, Arabic, and eighteen other language packs they’ll never touch. Switching languages triggers a lazy load of just that resource, on demand.
Translations live outside the app, on a CDN. We moved all translation JSON files out of the React build entirely and onto Amazon S3, served through a CDN. That gave us:
- Lower latency and faster downloads globally
- Strong cache utilization, so repeat visits barely touch the network
- High availability, independent of app deploys
- The ability to update copy without rebuilding or redeploying the app
That last point matters more than it sounds. A typo fix or a marketing copy tweak used to mean a full release cycle. Now it’s a file upload.
Built to scale to 20+ languages, including RTL. We standardized namespaces and key-naming conventions, organized resources into modular JSON structures, and built in full support for both left-to-right and right-to-left layouts — so typography, spacing, alignment, and UI behavior all adapt automatically based on language direction. Adding language 21 doesn’t require touching the architecture. It requires uploading a JSON file.
Get Fareed Ahmad’s stories in your inbox
Join Medium for free to get updates from this writer.
Automating the tedious part. The part nobody enjoys about i18n is the bookkeeping — making sure every new string added by every developer actually gets a key, and that key actually gets translated everywhere. We solved this with an npm scan script that traverses the codebase, detects newly introduced translation keys, and automatically updates the translation JSONs. No more missing translations discovered in QA. No more manual cross-referencing.
The Impact, in Numbers
This wasn’t a “nice architecture” exercise — it moved real metrics:
- ~40% reduction in frontend production bundle size
- Faster application startup, with less JS to parse and execute
- Lower browser memory usage, since unused languages are never loaded
- Improved First Contentful Paint (FCP) and Time to Interactive (TTI)
- Better caching efficiency via CDN-backed delivery
- Faster, lighter deployments, since translation content no longer inflates the build
- Zero-downtime translation updates — content changes ship independently of app releases
The story here isn’t just “the app got faster.” It’s that performance and global reach stopped being in tension with each other. Before, every new market we entered made the app slightly worse for everyone else. Now, growth is free.
Better for Users, Better for the Team
The performance wins were the headline, but the developer experience improvements are what made this sustainable.
- No more manually juggling translation files across a growing codebase.
- New keys are detected automatically — nothing falls through the cracks.
- Clear namespacing means large teams can work on localization in parallel without stepping on each other.
- Onboarding got simpler: new engineers don’t need to learn a bespoke translation workflow.
- Product and content teams can update copy without waiting on an engineering release.
Decoupling localization from application logic didn’t just make the app leaner — it made the system around the app easier to reason about.
What This Taught Us
A few principles worth carrying into any similar problem:
- Decouple content from code whenever it changes faster than your logic does. Translations, marketing copy, and configuration all fit this pattern — bundling them with your app just couples your release cycle to things that shouldn’t need one.
- Load what the user needs, not what the app supports. “We support 20 languages” doesn’t mean every user needs all 20 sitting in their browser memory.
- Treat translations as first-class assets, not an afterthought bolted on before launch. The moment you have a real content pipeline — versioned, cached, CDN-delivered — localization stops being a bottleneck.
- Global reach is an architecture decision, not a plugin you install. The libraries (i18next, react-i18next) were the easy part. The design decisions around loading strategy, hosting, and automation were what actually moved the needle.
Let’s Talk
Localization is one of those problems that looks simple until you’re supporting real scale — and then it quietly becomes one of the more consequential architecture decisions in your app.
I’d love to hear how other teams are handling this: Are you bundling translations, or serving them separately? Have you run into the same bundle-size creep as you added languages? And if there’s interest, I’m happy to go deeper into the technical setup — from the S3/CDN pipeline to the automated key-extraction workflow.
Drop a comment, or reach out. Always up for comparing notes on this.
#React #Internationalization #i18n #WebPerformance #FrontendDevelopment #SoftwareEngineering #ScalableArchitecture #CloudComputing #AWS #EngineeringLeadership #TechInnovation









