Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Reduce CanvasKit memory use on Safari
  • Loading branch information
via-guy committed May 11, 2026
commit f81d6fddbc2a82f85d676fb36f6784e565822366
4 changes: 4 additions & 0 deletions engine/src/flutter/lib/web_ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,9 @@ The Flutter Web engine's sources are in `localhost:<port>` > `lib` > `_engine` >
`engine`. You can set breakpoints in Dart source files and use the Chrome
debugger to inspect variables' values.

For Safari-specific CanvasKit memory behavior, see
[Safari CanvasKit memory][10].

## Building CanvasKit and Skwasm

To build CanvasKit and/or Skwasm locally, you must first set up your gclient config to
Expand Down Expand Up @@ -329,3 +332,4 @@ Note: The provided Dart SDK is used to run the `felt` tool itself, and to compil
[7]: https://developer.chrome.com/docs/devtools
[8]: https://developer.chrome.com/docs/devtools/sources
[9]: https://github.com/flutter/flutter/blob/main/docs/engine/contributing/Setting-up-the-Engine-development-environment.md#additional-steps-for-web-engine
[10]: docs/SAFARI_CANVASKIT_MEMORY.md
25 changes: 25 additions & 0 deletions engine/src/flutter/lib/web_ui/docs/SAFARI_CANVASKIT_MEMORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Safari CanvasKit memory

Flutter Web uses CanvasKit to render Flutter pictures into one or more canvas
surfaces. Safari's WebGL and canvas memory can remain resident longer than in
Chromium-based browsers, so a CanvasKit app that is stable in Chrome can have a
much higher physical footprint in Safari.

The web engine applies Safari-specific CanvasKit defaults to reduce this
footprint:

* `canvasKitForceCpuOnly` defaults to `true` in Safari. An app can still opt out
by setting `canvasKitForceCpuOnly` in `window.flutterConfiguration` or by
compiling with `FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY=false`.
* `canvasKitMaximumSurfaces` defaults to `2` in Safari. A single surface can
merge Flutter overlays with platform-view underlays, which can make platform
views such as Google Maps disappear behind Flutter content. Two surfaces keep
the underlay and overlay paths separate while reducing retained canvas state.
* The Safari CanvasKit rasterizer caps Skia's resource cache so GPU-backed
resources are not allowed to grow to the same size as other browsers.

When changing these defaults, validate both memory and platform-view
composition. A useful regression test is a release-mode CanvasKit app that
places Flutter content above a platform view, idles for at least 60 seconds, and
checks that the platform view remains visible while Safari WebContent memory
stays below the expected limit.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:js_interop';
import 'dart:math' as math;
import 'dart:typed_data';

import 'package:meta/meta.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
Expand All @@ -18,6 +19,9 @@ class CanvasKitRenderer extends Renderer {
static CanvasKitRenderer get instance => _instance;
static late CanvasKitRenderer _instance;

@visibleForTesting
static const int safariResourceCacheMaxBytes = 24 * 1024 * 1024;

Future<void>? _initialized;

@override
Expand All @@ -34,14 +38,22 @@ class CanvasKitRenderer extends Renderer {
FlutterFontCollection get fontCollection => _fontCollection;

static Rasterizer _createRasterizer() {
if (configuration.canvasKitForceMultiSurfaceRasterizer || isSafari || isFirefox) {
return MultiSurfaceRasterizer(
final Rasterizer rasterizer;
final bool useMultiSurfaceRasterizer =
configuration.canvasKitForceMultiSurfaceRasterizer || isFirefox || isSafari;
if (useMultiSurfaceRasterizer) {
rasterizer = MultiSurfaceRasterizer(
(OnscreenCanvasProvider canvasProvider) => CkOnscreenSurface(canvasProvider),
);
} else {
rasterizer = OffscreenCanvasRasterizer(
(OffscreenCanvasProvider canvasProvider) => CkOffscreenSurface(canvasProvider),
);
}
if (isSafari) {
rasterizer.setResourceCacheMaxBytes(safariResourceCacheMaxBytes);
}
return OffscreenCanvasRasterizer(
(OffscreenCanvasProvider canvasProvider) => CkOffscreenSurface(canvasProvider),
);
return rasterizer;
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ abstract class CkSurface extends Surface {
_skSurface = null;
}

void release() {}

@override
void setSkiaResourceCacheMaxBytes(int bytes) {
_grContext?.setResourceCacheLimitBytes(bytes.toDouble());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class DisplayCanvasFactory<T extends DisplayCanvas> {
'was not created by this factory',
);
canvas.hostElement.remove();
canvas.release();
_liveCanvases.remove(canvas);
_cache.add(canvas);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ abstract class DisplayCanvas {
/// Initialize the overlay.
void initialize();

/// Releases transient resources while keeping this canvas reusable.
void release() {}

/// Disposes this overlay.
void dispose();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class RenderCanvas extends DisplayCanvas {
final DomHTMLCanvasElement canvasElement = createDomCanvasElement();
int _pixelWidth = 0;
int _pixelHeight = 0;
bool _hasTransferredBitmap = false;

late final DomImageBitmapRenderingContext renderContext = canvasElement.contextBitmapRenderer;

Expand All @@ -73,6 +74,7 @@ class RenderCanvas extends DisplayCanvas {
void render(DomImageBitmap bitmap) {
_ensureSize(BitmapSize(bitmap.width, bitmap.height));
renderContext.transferFromImageBitmap(bitmap);
_hasTransferredBitmap = true;
}

void renderWithNoBitmapSupport(
Expand Down Expand Up @@ -126,8 +128,22 @@ class RenderCanvas extends DisplayCanvas {
// No extra initialization needed.
}

@override
void release() {
if (!_hasTransferredBitmap) {
return;
}
renderContext.transferFromImageBitmap(null);
_hasTransferredBitmap = false;
}

@override
void dispose() {
// No extra cleanup needed.
release();
_pixelWidth = 0;
_pixelHeight = 0;
canvasElement.width = 0;
canvasElement.height = 0;
_updateLogicalHtmlCanvasSize();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ abstract class SurfaceProvider<C extends Surface, D extends CanvasProvider> {

int? _resourceCacheMaxBytes;

@visibleForTesting
int? get debugResourceCacheMaxBytes => _resourceCacheMaxBytes;

void setSkiaResourceCacheMaxBytes(int bytes) {
_resourceCacheMaxBytes = bytes;
for (final C surface in _createdSurfaces) {
Expand Down
36 changes: 32 additions & 4 deletions engine/src/flutter/lib/web_ui/lib/src/engine/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ library configuration;
import 'dart:js_interop';

import 'package:meta/meta.dart';
import 'browser_detection.dart';
import 'dom.dart';

enum CanvasKitVariant {
Expand Down Expand Up @@ -288,10 +289,25 @@ class FlutterConfiguration {
///
/// This is mainly used for testing or for apps that want to ensure they
/// run on devices which don't support WebGL.
bool get canvasKitForceCpuOnly =>
_configuration?.canvasKitForceCpuOnly ?? _defaultCanvasKitForceCpuOnly;
bool get canvasKitForceCpuOnly {
final bool? configured = _configuration?.canvasKitForceCpuOnly;
if (configured != null) {
return configured;
}
if (_hasDefaultCanvasKitForceCpuOnly) {
return _defaultCanvasKitForceCpuOnly;
}
return isSafari;
}

static const String _canvasKitForceCpuOnlyEnvironment = 'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY';

static const bool _hasDefaultCanvasKitForceCpuOnly = bool.hasEnvironment(
_canvasKitForceCpuOnlyEnvironment,
);

static const bool _defaultCanvasKitForceCpuOnly = bool.fromEnvironment(
'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY',
_canvasKitForceCpuOnlyEnvironment,
);

bool get canvasKitForceMultiSurfaceRasterizer =>
Expand All @@ -301,11 +317,23 @@ class FlutterConfiguration {
'FLUTTER_WEB_CANVASKIT_FORCE_MULTI_SURFACE_RASTERIZER',
);

/// The default maximum number of canvases to use when rendering in CanvasKit.
@visibleForTesting
static const int defaultCanvasKitMaximumSurfaces = 8;

/// Safari needs at least two surfaces to keep platform-view underlays and
/// Flutter overlays separate, but more surfaces retain additional GPU state.
@visibleForTesting
static const int safariDefaultCanvasKitMaximumSurfaces = 2;

/// The maximum number of canvases to use when rendering in CanvasKit.
///
/// Limits the amount of overlays that can be created.
int get canvasKitMaximumSurfaces {
final int maxSurfaces = _configuration?.canvasKitMaximumSurfaces?.toInt() ?? 8;
final int defaultMaxSurfaces = isSafari
? safariDefaultCanvasKitMaximumSurfaces
: defaultCanvasKitMaximumSurfaces;
final int maxSurfaces = _configuration?.canvasKitMaximumSurfaces?.toInt() ?? defaultMaxSurfaces;
if (maxSurfaces < 1) {
return 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ class PlatformViewManager {
/// never been rendered before.
void clearPlatformView(int viewId) {
// Remove from our cache, and then from the DOM...
_viewIdToType.remove(viewId);
_contents.remove(viewId)?.remove();
}

Expand Down
43 changes: 33 additions & 10 deletions engine/src/flutter/lib/web_ui/test/canvaskit/rasterizer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:js_interop';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;

import 'common.dart';

Expand All @@ -16,16 +17,38 @@ void main() {

void testMain() {
setUpCanvasKitTest(withImplicitView: true);
test(
'defaults to OffscreenCanvasRasterizer on Chrome and MultiSurfaceRasterizer on Firefox and Safari',
() {
if (isChromium) {
expect(CanvasKitRenderer.instance.rasterizer, isA<OffscreenCanvasRasterizer>());
} else {
expect(CanvasKitRenderer.instance.rasterizer, isA<MultiSurfaceRasterizer>());
}
},
);

tearDown(() {
ui_web.browser.debugBrowserEngineOverride = null;
ui_web.browser.debugOperatingSystemOverride = null;
CanvasKitRenderer.instance.debugResetRasterizer();
});

test('defaults to OffscreenCanvasRasterizer on Chrome and MultiSurfaceRasterizer on Firefox', () {
if (isChromium) {
expect(CanvasKitRenderer.instance.rasterizer, isA<OffscreenCanvasRasterizer>());
} else {
expect(CanvasKitRenderer.instance.rasterizer, isA<MultiSurfaceRasterizer>());
}
});

test('defaults to MultiSurfaceRasterizer on desktop Safari', () {
ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit;
ui_web.browser.debugOperatingSystemOverride = ui_web.OperatingSystem.macOs;

CanvasKitRenderer.instance.debugResetRasterizer();

expect(CanvasKitRenderer.instance.rasterizer, isA<MultiSurfaceRasterizer>());
});

test('defaults to MultiSurfaceRasterizer on iOS Safari', () {
ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit;
ui_web.browser.debugOperatingSystemOverride = ui_web.OperatingSystem.iOs;

CanvasKitRenderer.instance.debugResetRasterizer();

expect(CanvasKitRenderer.instance.rasterizer, isA<MultiSurfaceRasterizer>());
});

test('can be configured to always use MultiSurfaceRasterizer', () {
debugOverrideJsConfiguration(
Expand Down
43 changes: 42 additions & 1 deletion engine/src/flutter/lib/web_ui/test/canvaskit/renderer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:test/test.dart';

import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;

import 'common.dart';

Expand Down Expand Up @@ -83,6 +84,8 @@ void testMain() {
setUpCanvasKitTest();

tearDown(() {
ui_web.browser.debugBrowserEngineOverride = null;
ui_web.browser.debugOperatingSystemOverride = null;
CanvasKitRenderer.instance.debugResetRasterizer();
});

Expand Down Expand Up @@ -190,7 +193,7 @@ void testMain() {
});

test(
'defaults to OffscreenCanvasRasterizer on Chrome and MultiSurfaceRasterizer on Firefox and Safari',
'defaults to OffscreenCanvasRasterizer on Chrome and MultiSurfaceRasterizer on Firefox',
() {
if (isChromium) {
expect(CanvasKitRenderer.instance.rasterizer, isA<OffscreenCanvasRasterizer>());
Expand All @@ -200,6 +203,44 @@ void testMain() {
},
);

test('uses MultiSurfaceRasterizer and caps Skia resource cache on desktop Safari', () {
ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit;
ui_web.browser.debugOperatingSystemOverride = ui_web.OperatingSystem.macOs;

CanvasKitRenderer.instance.debugResetRasterizer();

expect(CanvasKitRenderer.instance.rasterizer, isA<MultiSurfaceRasterizer>());
expect(
CanvasKitRenderer.instance.rasterizer.surfaceProvider.debugResourceCacheMaxBytes,
CanvasKitRenderer.safariResourceCacheMaxBytes,
);
});

test('uses MultiSurfaceRasterizer and caps Skia resource cache on iOS Safari', () {
ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit;
ui_web.browser.debugOperatingSystemOverride = ui_web.OperatingSystem.iOs;

CanvasKitRenderer.instance.debugResetRasterizer();

expect(CanvasKitRenderer.instance.rasterizer, isA<MultiSurfaceRasterizer>());
expect(
CanvasKitRenderer.instance.rasterizer.surfaceProvider.debugResourceCacheMaxBytes,
CanvasKitRenderer.safariResourceCacheMaxBytes,
);
});

test('does not cap Skia resource cache on Chromium', () {
ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.blink;

CanvasKitRenderer.instance.debugResetRasterizer();

expect(CanvasKitRenderer.instance.rasterizer, isA<OffscreenCanvasRasterizer>());
expect(
CanvasKitRenderer.instance.rasterizer.surfaceProvider.debugResourceCacheMaxBytes,
isNull,
);
});

test('can be configured to always use MultiSurfaceRasterizer', () {
debugOverrideJsConfiguration(
<String, Object?>{'canvasKitForceMultiSurfaceRasterizer': true}.jsify()
Expand Down
Loading
Loading