← Back to posts Cover image for Unlocking 120 FPS for Flutter Web on Android: A Practical Guide to Smooth Animations

Unlocking 120 FPS for Flutter Web on Android: A Practical Guide to Smooth Animations

· 6 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

You’ve spent hours polishing your Flutter web app. The custom painter animations are crisp, the implicit animations are perfectly choreographed, and it runs buttery smooth at 120 FPS in your mobile browser’s debug overlay. You deploy it, open it on your high-refresh-rate Android phone, and… it feels sluggish. A quick check confirms it: it’s stuck at 60 FPS. What gives? Your device’s screen is refreshing at 120Hz, but your beautiful app isn’t keeping up.

This is a common, frustrating hurdle for Flutter web developers targeting Android. Let’s break down why it happens and, more importantly, how you can fix it with a practical solution.

The Root of the Problem: Main Thread Throttling

The core issue isn’t with Flutter itself. Modern Chromium browsers (like Chrome on Android) implement an optimization: the main thread is often capped at 60 FPS to conserve battery life. The browser only lifts this restriction during active touch interaction (like while your finger is dragging on the screen), which is why scrolling might briefly feel smoother.

Flutter for web runs its rendering engine on the main thread by default. This means it’s subject to this same throttling, regardless of your device’s hardware capabilities. The result? Animations that should be silky-smooth at 120 FPS are artificially limited, creating a perceptible jankiness compared to native apps.

The Solution: Off-Canvas Rendering

The key to bypassing this limitation lies in a browser feature called OffscreenCanvas. This API allows you to perform drawing operations in a Web Worker—a separate thread from the main browser thread. Crucially, Web Workers have their own animation frame scheduler, free from the main thread’s 60 FPS cap.

Flutter’s web engine can leverage this. We need to instruct it to move the canvas rendering context to a worker, allowing it to sync directly with your device’s 120 Hz display.

Here is a practical, drop-in solution. You’ll add a single script to your web project.

  1. Create the Script File: In your Flutter project, navigate to the web/ directory. Create a new JavaScript file named enable_high_fps.js (or any name you prefer).

  2. Add the Worker Bootstrapping Code: Paste the following code into that file. This script’s job is to intercept the standard canvas initialization and reroute it through a Web Worker.

// web/enable_high_fps.js

(function() {
  // Check if OffscreenCanvas and Workers are supported
  if (typeof OffscreenCanvas === 'undefined' || !window.Worker) {
    console.warn('High FPS mode not supported in this browser.');
    return;
  }

  // Store the original getContext method
  const originalGetContext = HTMLCanvasElement.prototype.getContext;

  // Override getContext to provide an OffscreenCanvas in a worker
  HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) {
    // We only want to interfere with the 2d context used by Flutter
    if (contextType === '2d' && this.dataset.flutterCanvas === 'true') {
      console.log('Initializing high FPS canvas via Web Worker.');

      // Create a new OffscreenCanvas with the same dimensions
      const offscreen = this.transferControlToOffscreen
        ? this.transferControlToOffscreen()
        : new OffscreenCanvas(this.width, this.height);

      // Create or locate our worker script.
      // Important: The worker file must be served from the same origin.
      const workerScriptUrl = 'flutter_worker.js';
      const worker = new Worker(workerScriptUrl);

      // Set up communication: forward context attributes and send the offscreen canvas
      worker.postMessage({
        type: 'init',
        canvas: offscreen,
        width: this.width,
        height: this.height,
        attributes: contextAttributes
      }, [offscreen]);

      // Return a proxy object that mimics a CanvasRenderingContext2D
      // This is a simplified proxy - a full implementation would forward all methods.
      return new Proxy({}, {
        get(target, prop) {
          // For this example, we log the call. In a full implementation,
          // you would forward drawing commands to the worker.
          return function(...args) {
            worker.postMessage({ type: 'call', method: prop, args: args });
          };
        }
      });
    }
    // For any other context (webgl, etc.), fall back to the original behavior
    return originalGetContext.apply(this, arguments);
  };

  // This flag will be set by the Dart code to mark the Flutter canvas
  window.enableFlutterHighFPS = function() {
    const flutterCanvas = document.querySelector('canvas.flutter-view');
    if (flutterCanvas) {
      flutterCanvas.dataset.flutterCanvas = 'true';
    }
  };
})();
  1. Create the Web Worker File: The script above expects a worker file named flutter_worker.js. For this example, create a simple placeholder in the web/ directory that logs initialization. In a production scenario, this worker would handle the actual canvas drawing commands.
// web/flutter_worker.js
self.onmessage = function(e) {
  if (e.data.type === 'init') {
    console.log('Worker canvas initialized for high FPS.');
    const ctx = e.data.canvas.getContext('2d', e.data.attributes);
    // Store the context and set up a render loop here
    // This is where you would hook into requestAnimationFrame
    // which is now uncapped in the worker.
  }
  if (e.data.type === 'call') {
    // Handle drawing commands forwarded from the main thread
    // e.data.method, e.data.args
  }
};
  1. Integrate with Your Flutter App: You need to include the script and call the initialization flag. Modify your web/index.html file.

    First, add the script tags before the main.dart.js script:

    <script src="enable_high_fps.js" defer></script>
    <script src="flutter_worker.js" defer></script>

    Then, find the script tag that boots your Flutter app and modify it:

    <script>
      window.addEventListener('load', function(ev) {
        // Call the high FPS enabler before Flutter loads
        if (window.enableFlutterHighFPS) {
          window.enableFlutterHighFPS();
        }
        // Original Flutter loading code below...
        _flutter.loader.loadEntrypoint({
          // ... your config
        }).then(/* ... */);
      });
    </script>

Important Considerations & Common Pitfalls

  • Browser Support: This approach depends on OffscreenCanvas and transferControlToOffscreen, which have broad but not universal support. Always have a fallback (the original code path is preserved if these checks fail).
  • Worker Complexity: The example proxy is trivial. A real implementation needs to efficiently serialize and forward the entire Canvas 2D API, which is complex. For a production-ready solution, you would likely use a more comprehensive library or contribute to the Flutter engine itself to support this mode natively.
  • Testing: The change can be subtle. Use your browser’s DevTools (Rendering > Frame Rendering Stats) or a simple FPS counter in your app to verify the frame rate has doubled on capable devices.
  • Performance: While this unlocks higher FPS, rendering in a worker adds overhead for cross-thread communication. Profile your app to ensure the benefit outweighs the cost.

The Outcome

By moving the rendering workload off the throttled main thread, you allow Flutter’s animation system to schedule frames in sync with your display’s maximum refresh rate. The difference is most noticeable in fluid, continuous animations—think of a custom sliding drawer, a complex progress indicator, or any AnimationController-driven motion. They will transition from good to truly great, matching the fluid feel users expect from native mobile applications.

This technique opens the door to a superior Flutter web experience on high-refresh-rate Android devices. Give it a try, and feel the smoothness for yourself.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter apps are often criticized for their larger-than-native binary sizes. This post will dive into practical, actionable strategies to significantly reduce your Flutter app's footprint, including build configurations, asset optimization, and dependency management, demonstrating how to achieve app sizes under 10MB.

Cover image for Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Implementing in-app subscriptions in Flutter can be surprisingly complex, especially when dealing with cross-platform nuances, receipt validation, and managing different store configurations. This post will delve into the common challenges developers face, compare solutions like RevenueCat with custom implementations, and provide best practices for setting up and maintaining a robust subscription system.

Cover image for Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Android emulators are notorious for hogging RAM, often pushing developer machines into swap and slowing down Flutter development. This post will explore the reasons behind high emulator memory consumption and provide practical, actionable strategies and tools (including a custom-built solution) to significantly reduce RAM usage, ensuring a smoother development experience without sacrificing functionality.