DEV Community

Cover image for Versioned Deploys for Flutter Web: Fast Loads, Instant Updates
Serge Matveenko
Serge Matveenko

Posted on

Versioned Deploys for Flutter Web: Fast Loads, Instant Updates

Flutter Web apps are fast—until the browser cache gets in the way. Anyone who’s deployed a new version knows the pain: users stuck on stale bundles, missing features, or broken screens until they hit a hard refresh.

This post walks through a proven versioned deploy pattern for Flutter Web, using Nginx and simple cache rules to make updates instant and repeat loads nearly instantaneous.


This approach is especially critical for embedded Flutter Web apps like Telegram Mini Apps or other environments that heavily cache web views. Platforms such as Telegram can aggressively cache app bundles—sometimes so persistently that developers need to completely reinstall the app to clear stale versions. Using versioned deploys ensures updates reach users immediately, even in these restrictive caching environments.

How It All Works

Before we dive deeper, let’s unpack what’s happening under the hood:

  • Browsers love caching. When they see a file like main.dart.js, they assume it rarely changes and keep serving it from local storage. That’s great for speed—until you ship an update.
  • Flutter builds are static bundles. After flutter build web, you get JavaScript (main.dart.js), WebAssembly (canvaskit.wasm), and other assets. These are ideal for long-term caching.
  • Nginx controls caching via HTTP headers. By attaching Cache-Control and immutable directives, we tell browsers how long to keep files and when to refetch them.
  • Version folders act as unique cache keys. Instead of trying to invalidate caches, we generate a new folder name per release (like /v1.0.1/). The browser automatically fetches the new files since the URLs have changed.

For example:

  • On your first visit, you might download /v1.0.0/main.dart.js.
  • After a new deploy, the page now references /v1.0.1/main.dart.js.
  • The browser sees it as a totally new file and fetches it fresh.

This technique avoids conflicts between versions and keeps old assets available for rollbacks or debugging.

Why Flutter Web Needs This

When you build Flutter for the web, it produces a set of files like:

main.dart.js
flutter_service_worker.js
assets/
canvaskit/
Enter fullscreen mode Exit fullscreen mode

They’re large but static—perfect for long-term caching. The problem? Browsers keep serving the old ones after each deploy, even when your app should update.

We fix that by separating mutable entry points from immutable versioned assets.


Architecture Overview

Entry Points (never cached)         Versioned Assets (aggressively cached)
├─ index.html                       ├─ /v1.0.0/main.dart.js
├─ config.json                      ├─ /v1.0.0/flutter_service_worker.js
└─ flutter_bootstrap.js             └─ /v1.0.0/canvaskit/canvaskit.wasm
Enter fullscreen mode Exit fullscreen mode
  • Entry points — lightweight files that load the app. Always fetched fresh.
  • Versioned assets — compiled Flutter bundles served from a versioned folder like /v1.0.0/. Cached for a year with immutable.

Each new deploy introduces a new folder, effectively cache-busting automatically.


The Bootstrap Logic

The bootstrap script handles version discovery and initializes Flutter with the correct paths:

(function () {
  'use strict';
  fetch('/config.json', { cache: 'no-store' })
    .then(r => r.json())
    .then(cfg => {
      const version = cfg.version || 'local';
      const base = version === 'local' ? '/' : `/${version}/`;
      const userConfig = { assetBase: base, entrypointBaseUrl: base };
      console.log('Loading Flutter app from:', base);
      _flutter.loader.load({ config: userConfig });
    })
    .catch(err => console.error('Bootstrap error', err));
})();
Enter fullscreen mode Exit fullscreen mode

config.json

{
  "version": "v1.0.0",
  "apiUrl": "https://api.example.com"
}
Enter fullscreen mode Exit fullscreen mode

Every deploy updates the version field. The Flutter loader then fetches from the corresponding folder.


Nginx Configuration

Here’s the minimal Nginx setup to make this work:

# Never cache entry points
location = /index.html {
    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
location = /config.json {
    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
location = /flutter_bootstrap.js {
    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}

# Aggressively cache versioned assets
location ~ ^/([^/]+)/(.*)$ {
    set $req_version $1;
    set $asset_path $2;

    if ($req_version != $active_version) {
        return 404;
    }

    add_header Cache-Control "public, max-age=31536000, immutable";
    try_files /$asset_path =404;
}
Enter fullscreen mode Exit fullscreen mode

$active_version can be set via an environment variable or included file so Nginx knows which version is current.

This ensures only the active version is served, preventing old assets from being reused after deployment.


Cache Behavior

Entry Points

Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0
Enter fullscreen mode Exit fullscreen mode
  • Always fetched from the network.
  • Detects new versions immediately.

Versioned Assets

Cache-Control: public, max-age=31536000, immutable
Enter fullscreen mode Exit fullscreen mode
  • Cached for one year.
  • Never revalidated while version remains the same.

New version = new folder = automatic cache bust.


What Users Experience

First Visit

  • Loads entry points and versioned assets.
  • Flutter initializes normally.

Second Visit (Same Version)

  • Entry points fetched fresh.
  • Bulk assets loaded instantly from cache.

After Deployment

  • Entry points detect new version.
  • App loads new version’s assets automatically.
  • Old cached files remain but aren’t used.

Zero downtime, no forced refresh.


Best Practices

  • Use semantic versions like v1.2.3.
  • Keep entry points small for fast version detection.
  • Validate versions in Nginx to avoid stale assets.
  • Log the version in the browser console for debugging.
  • Purge or rotate old folders occasionally to save storage.

Why It Works

  1. Version in URL = cache key — instant cache busting.
  2. Immutable assets — best‑case caching for performance.
  3. Fresh entry points — ensures the browser always learns about updates.
  4. Server‑side validation — protects against stale or mismatched bundles.

This simple pattern turns Flutter Web deploys from a headache into a smooth, predictable process—all with standard Nginx and a few lines of JavaScript.

Top comments (0)