Flutter Localization Performance: Optimize Your Multi-Language App
Learn how to optimize Flutter localization for maximum performance. Reduce app size, improve load times, and deliver smooth experiences in multi-language apps.
Why Localization Performance Matters
A poorly optimized localized app can suffer from:
- Slow startup times - Loading all translations upfront
- Increased app size - Bundling unused languages
- Memory bloat - Keeping all locales in memory
- Janky UI - Synchronous locale switching
Let's fix each of these issues.
1. Lazy Load Translations
Instead of loading all translations at startup, load them on demand:
class LazyLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
final Map<Locale, AppLocalizations> _cache = {};
@override
bool isSupported(Locale locale) => ['en', 'es', 'fr', 'de', 'ar'].contains(locale.languageCode);
@override
Future<AppLocalizations> load(Locale locale) async {
if (_cache.containsKey(locale)) {
return _cache[locale]!;
}
// Load only the needed locale
final localizations = await AppLocalizations.load(locale);
_cache[locale] = localizations;
return localizations;
}
@override
bool shouldReload(covariant LocalizationsDelegate old) => false;
}
This approach:
- Loads only the current locale at startup
- Caches loaded locales for instant switching
- Reduces initial load time significantly
2. Split Large Translation Files
For apps with thousands of strings, split translations by feature:
lib/l10n/
├── app_en.arb # Core strings (~100 keys)
├── app_es.arb
├── features/
│ ├── auth_en.arb # Auth module (~50 keys)
│ ├── auth_es.arb
│ ├── settings_en.arb # Settings module (~80 keys)
│ └── settings_es.arb
Load feature translations only when entering that module:
class SettingsScreen extends StatefulWidget {
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
late Future<SettingsLocalizations> _localizations;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final locale = Localizations.localeOf(context);
_localizations = SettingsLocalizations.load(locale);
}
@override
Widget build(BuildContext context) {
return FutureBuilder<SettingsLocalizations>(
future: _localizations,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
return SettingsContent(l10n: snapshot.data!);
},
);
}
}
3. Reduce App Bundle Size
Use Deferred Components (Android)
Split locales into separate download modules:
# pubspec.yaml
flutter:
deferred-components:
- name: spanish_translations
libraries:
- package:my_app/l10n/es.dart
- name: french_translations
libraries:
- package:my_app/l10n/fr.dart
Users download additional languages only when needed.
Remove Unused Locales
Don't include locales you don't actively support:
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
# Only generate what you need
supported-locales:
- en
- es
- fr
4. Optimize String Lookups
Avoid Repeated Lookups
Bad - looks up localization multiple times:
Widget build(BuildContext context) {
return Column(
children: [
Text(AppLocalizations.of(context)!.title),
Text(AppLocalizations.of(context)!.subtitle),
Text(AppLocalizations.of(context)!.description),
Text(AppLocalizations.of(context)!.footer),
],
);
}
Good - single lookup, reuse the reference:
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
Text(l10n.title),
Text(l10n.subtitle),
Text(l10n.description),
Text(l10n.footer),
],
);
}
Use const Where Possible
For static localized widgets, consider const constructors:
class LocalizedTitle extends StatelessWidget {
const LocalizedTitle({super.key});
@override
Widget build(BuildContext context) {
return Text(AppLocalizations.of(context)!.appTitle);
}
}
// Usage - widget is const, only text changes
const LocalizedTitle()
5. Async Locale Switching
Prevent UI jank during locale changes:
class LocaleProvider extends ChangeNotifier {
Locale _locale = const Locale('en');
bool _isLoading = false;
Locale get locale => _locale;
bool get isLoading => _isLoading;
Future<void> setLocale(Locale newLocale) async {
if (_locale == newLocale) return;
_isLoading = true;
notifyListeners();
// Preload the new locale
await AppLocalizations.delegate.load(newLocale);
_locale = newLocale;
_isLoading = false;
notifyListeners();
}
}
Show a loading indicator during switch:
Consumer<LocaleProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return child!;
},
child: const MyApp(),
)
6. Measure Localization Performance
Profile Startup Time
void main() {
final stopwatch = Stopwatch()..start();
runApp(
Builder(
builder: (context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
print('App ready in ${stopwatch.elapsedMilliseconds}ms');
});
return const MyApp();
},
),
);
}
Track Locale Switch Time
Future<void> switchLocale(Locale newLocale) async {
final stopwatch = Stopwatch()..start();
await localeProvider.setLocale(newLocale);
print('Locale switch took ${stopwatch.elapsedMilliseconds}ms');
}
Monitor Memory Usage
import 'dart:developer' as developer;
void checkMemory() {
developer.Timeline.startSync('Memory Check');
// Your locale loading code
developer.Timeline.finishSync();
}
7. Caching Strategies
Memory Cache with LRU
Keep only recently used locales in memory:
class LRULocaleCache {
final int maxSize;
final Map<Locale, AppLocalizations> _cache = {};
final List<Locale> _accessOrder = [];
LRULocaleCache({this.maxSize = 3});
AppLocalizations? get(Locale locale) {
if (_cache.containsKey(locale)) {
// Move to end (most recently used)
_accessOrder.remove(locale);
_accessOrder.add(locale);
return _cache[locale];
}
return null;
}
void put(Locale locale, AppLocalizations localizations) {
if (_cache.length >= maxSize && !_cache.containsKey(locale)) {
// Remove least recently used
final lru = _accessOrder.removeAt(0);
_cache.remove(lru);
}
_cache[locale] = localizations;
_accessOrder.add(locale);
}
}
Disk Cache for Offline
Cache translations locally for offline use:
class PersistentLocaleCache {
Future<void> cacheLocale(Locale locale, Map<String, String> translations) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(
'locale_${locale.languageCode}',
jsonEncode(translations),
);
}
Future<Map<String, String>?> getLocale(Locale locale) async {
final prefs = await SharedPreferences.getInstance();
final data = prefs.getString('locale_${locale.languageCode}');
if (data != null) {
return Map<String, String>.from(jsonDecode(data));
}
return null;
}
}
8. Build-Time Optimizations
Tree Shaking Unused Keys
Use flutter gen-l10n with unused key removal:
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
untranslated-messages-file: untranslated.txt
Review untranslated.txt and remove unused keys from your ARB files.
Compress ARB Files
Minify ARB files for production:
# Remove whitespace and comments
jq -c 'del(.["@@locale"]) | del(.["@@last_modified"])' app_en.arb > app_en.min.arb
Performance Benchmarks
| Optimization | Before | After | Improvement |
|---|---|---|---|
| Lazy loading | 850ms | 320ms | 62% faster |
| LRU cache | 45MB | 18MB | 60% less memory |
| Deferred components | 12MB | 4MB | 67% smaller APK |
| Single lookup | 16ms | 4ms | 75% faster render |
Common Performance Mistakes
1. Loading All Locales at Startup
// Bad - loads everything
for (final locale in supportedLocales) {
await AppLocalizations.delegate.load(locale);
}
2. Recreating Localizations on Every Build
// Bad - creates new instance every build
Widget build(BuildContext context) {
final l10n = AppLocalizations(); // Wrong!
return Text(l10n.title);
}
3. Blocking UI During Locale Switch
// Bad - blocks the main thread
void switchLocale(Locale locale) {
final l10n = AppLocalizations.loadSync(locale); // Blocks!
setState(() => _locale = locale);
}
Optimize with FlutterLocalisation
Managing localization performance at scale is complex. FlutterLocalisation.com helps you:
- Identify unused keys - Remove bloat from your ARB files
- Analyze translation size - See which locales are largest
- Optimize exports - Generate minified, production-ready ARB files
- Track coverage - Ensure all languages are complete
Stop guessing about localization performance. Try FlutterLocalisation free and optimize your multi-language Flutter app.
Summary
Optimize Flutter localization performance by:
- Lazy load - Load translations on demand
- Split files - Separate translations by feature
- Reduce size - Use deferred components, remove unused locales
- Cache smartly - LRU memory cache, disk cache for offline
- Measure - Profile startup, switch times, and memory
With these optimizations, your multi-language Flutter app will be fast for users everywhere.