← Back to Blog

Flutter Localization Performance: Optimize Your Multi-Language App

flutterlocalizationperformanceoptimizationapp-sizelazy-loading

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:

  1. Lazy load - Load translations on demand
  2. Split files - Separate translations by feature
  3. Reduce size - Use deferred components, remove unused locales
  4. Cache smartly - LRU memory cache, disk cache for offline
  5. Measure - Profile startup, switch times, and memory

With these optimizations, your multi-language Flutter app will be fast for users everywhere.