[web] Repair RenderCanvas CSS size drift#188797
Conversation
harryterkelsen
left a comment
There was a problem hiding this comment.
Hi @MarlonJD, thank you so much for putting together this fix! The logical canvas size drift on Safari under Skwasm is a critical issue and this is a very elegant way to self-heal.
During our review and testing across Chrome, Firefox, and Safari on both compilers (dart2js and dart2wasm), we identified a critical performance issue with exact string matching:
The Layout Thrashing Issue
By comparing the expected style string directly against the DOM style:
style.width == logicalSize.widthwe run into browser-specific floating-point normalization and rounding differences:
- Chrome & Firefox parse and round logical styles to 3 decimal places (e.g.
400.3333333px->400.333px). - Safari rounds them to 6 decimal places (e.g.
400.3333333px->400.333333px). - dart2wasm stringifies doubles with
.0(e.g.400.0px), whereas browsers normalize this to400px.
This causes the comparison style.width == logicalSize.width to evaluate to false on every single frame for any fractional layouts or integer sizes compiled with dart2wasm. Consequently, the engine writes to the canvas element's inline styles on every single frame, triggering continuous browser layout invalidation and thrashing, which severely hurts frame rendering performance.
Proposed Solution
Instead of exact string matching, we can parse the pixel lengths from the style declaration and perform a tolerance-based (epsilon) comparison (e.g., matching within a 0.01px threshold).
Here is a suggested modification:
bool _isLogicalHtmlCanvasSizeCurrent() {
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
if (devicePixelRatio != _currentDevicePixelRatio) {
return false;
}
final DomCSSStyleDeclaration style = canvasElement.style;
final double? actualWidth = _parseLogicalLength(style.width);
final double? actualHeight = _parseLogicalLength(style.height);
if (actualWidth == null || actualHeight == null) {
return false;
}
final double expectedWidth = _pixelWidth / devicePixelRatio;
final double expectedHeight = _pixelHeight / devicePixelRatio;
// Allow a small threshold (e.g. 0.01px) to account for browser float precision/rounding differences.
return (actualWidth - expectedWidth).abs() < 0.01 &&
(actualHeight - expectedHeight).abs() < 0.01;
}
double? _parseLogicalLength(String value) {
final String trimmed = value.trim();
if (!trimmed.endsWith('px')) {
return null;
}
return double.tryParse(trimmed.substring(0, trimmed.length - 2).trim());
}Could you update the PR to use a tolerance-based approach like this? We would love to get this landed!
|
Done, thanks! Updated the canvas CSS size check to parse px values and compare with a small tolerance, and added regression coverage for normalized inline CSS values. |
|
Thanks, LGTM! Can you take this out of draft status so I can start landing it? |
|
Hey @harryterkelsen, GitHub is not allowing me to mark the PR ready for review from my account, and I can’t open a replacement non-draft PR because I’m currently at Flutter’s 2 open PR limit. Could a maintainer please mark this one ready for review? |
|
It won't let me manually set it to "Ready for Review" because of the open PR limit. Can you make one of your open ones a draft and we'll land this one to "free up" your space? If not, then just ping me once one of your other PRs are closed so we can land this one |
|
Thanks @harryterkelsen I moved my other open PRs back to draft to free up the active PR slot, and this PR is now marked ready for review. |
There was a problem hiding this comment.
Code Review
This pull request updates RenderCanvas to repair its logical HTML canvas size when the inline CSS size drifts or when the device pixel ratio changes, introducing a tolerance check. New tests were added to verify this behavior. The review feedback highlights that overriding the global devicePixelRatio in these tests without restoring it can cause test pollution, and suggests using addTearDown to reset the ratio.
|
autosubmit label was removed for flutter/flutter/188797, because The base commit of the PR is older than 7 days and can not be merged. Please merge the latest changes from the main into this branch and resubmit the PR. |
|
autosubmit label was removed for flutter/flutter/188797, because This PR has not met approval requirements for merging. The PR author is not a member of flutter-hackers and needs 1 more review(s) in order to merge this PR.
|
|
autosubmit label was removed for flutter/flutter/188797, because The base commit of the PR is older than 7 days and can not be merged. Please merge the latest changes from the main into this branch and resubmit the PR. |
|
autosubmit label was removed for flutter/flutter/188797, because This PR has not met approval requirements for merging. The PR author is not a member of flutter-hackers and needs 1 more review(s) in order to merge this PR.
|
|
autosubmit label was removed for flutter/flutter/188797, because - The status or check suite Mac_x64 framework_tests_misc has failed. Please fix the issues identified (or deflake) before re-applying this label. |
flutter/flutter@dc2a870...f7b66f3 2026-07-10 [email protected] [iOS][test] Fix VSyncClient display link deallocation test on iOS 27 (flutter/flutter#188627) 2026-07-10 [email protected] Fix broken listener code in WindowScope (flutter/flutter#189208) 2026-07-10 [email protected] [web] Preserve text field focus across tab switches (flutter/flutter#188738) 2026-07-10 [email protected] [Impeller] Recycle HostBuffer arena entries only after GPU completion (flutter/flutter#188965) 2026-07-10 [email protected] Assert TextStyle height is not NaN (flutter/flutter#186617) 2026-07-10 [email protected] Roll Fuchsia Test Scripts from dFkTCiDxEtPxYK5Nn... to wLST_A-xfOeGT_5mj... (flutter/flutter#189259) 2026-07-10 [email protected] Roll Dart SDK from a11fb7ed40a5 to 0fc1668c4af4 (5 revisions) (flutter/flutter#189257) 2026-07-10 [email protected] Adds a missing `await` to a `FutureOr` (flutter/flutter#189198) 2026-07-10 [email protected] [web] Repair RenderCanvas CSS size drift (flutter/flutter#188797) 2026-07-10 [email protected] Roll Fuchsia Test Scripts from s5_gZFJ8De9AJalTw... to dFkTCiDxEtPxYK5Nn... (flutter/flutter#189215) 2026-07-10 [email protected] Roll Fuchsia Linux SDK from QcRFUtvCw2EobfJ8s... to czpzDg9ABY2oKLAOY... (flutter/flutter#189222) 2026-07-10 [email protected] Update Depdencies Used By `flutter_engine_group_performance` (flutter/flutter#189229) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC [email protected],[email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
What changed
This PR makes the stock
RenderCanvaspath repair stale inline canvas CSS size even when the backing-store size and DPR have not changed.Changes:
style.width/style.heightbefore the same-size early return.1206 x 2142 @ DPR 3, repairing0.333333pxback to402 x 714px.Why
#188723 tracks a Safari/WebKit Skwasm case where the canvas backing store can be valid while the visible CSS/display rect remains collapsed around
0.328125 x 0.328125px.The existing stock
RenderCanvascode repairs CSS size on physical-size changes and DPR changes, but not when only the inline CSS value is stale. This PR makes that same-size path self-heal.Scope
This does not enable Skwasm by default, change WebKit policy, depend on the multi-surface rasterizer, or by itself close all Safari/WebKit default-policy gates.
Validation
dart format: passed.git diff --check: passed.feltbrowser test in the [web] Repair RenderCanvas CSS size drift #188797 checkout: passed locally.DART_SUPPRESS_ANALYTICS=true HOME=/private/tmp DART_SDK_DIR=/private/tmp/flutter-mobile-safari-skwasm-canvas-css-size-fix-upstream-worktree/engine/src/flutter/third_party/dart/tools/sdks/dart-sdk ./dev/felt test --gcs-prod --browser chrome --compiler dart2js --renderer canvaskit --fail-early test/engine/compositing/render_canvas_test.dartchrome-dart2js-canvaskit-engine,3/3,All tests passed!.engine/src/out/wasm_release/flutter_web_sdkbuild from the [web] Repair RenderCanvas CSS size drift #188797 worktree: passed locally.dev/benchmarks/macrobenchmarksbuilt as web wasm/profile using the localwasm_releaseSDK: passed locally.FLUTTER_WEB_SKWASM_FORCE_MULTI_SURFACE_RASTERIZER; the string was absent.variant=none, no forced CSS sizing,bench_card_infinite_scroll:probe-complete,errors: [], loadedskwasm_heavy,main.dart.mjs, andmain.dart.wasm,crossOriginIsolated=true, canvas backing1206 x 2142, CSS/display/style402 x 714/402px x 714px.variant=none, no forced CSS sizing,bench_card_infinite_scroll:all-benchmarks-complete,errors: [], loadedskwasm_heavy,main.dart.mjs, andmain.dart.wasm, canvas backing1206 x 2142, CSS/display/style402 x 714/402px x 714px.variant=none, no forced CSS sizing,bench_simple_lazy_text_scroll:all-benchmarks-complete,errors: [], loadedskwasm_heavy,main.dart.mjs, andmain.dart.wasm, canvas backing1206 x 2142, CSS/display/style402 x 714/402px x 714px.Physical iPhone page telemetry reported
crossOriginIsolated=falseeven though the harness served isolation headers. Skwasm still loaded through the WebKit allowlist, so this is local physical geometry/benchmark-completion evidence, not a cross-origin-isolation pass.Related issue: #188723.
Broader Safari/WebKit Wasm/Skwasm tracking: #178893.