Skip to content

fix(chat): build the WebView settings once per surface, not per rebuild - #390

Merged
hydall merged 1 commit into
nightlyfrom
fix/webview-settings-realloc
Sep 5, 2026
Merged

fix(chat): build the WebView settings once per surface, not per rebuild#390
hydall merged 1 commit into
nightlyfrom
fix/webview-settings-realloc

Conversation

@hydall

@hydall hydall commented Sep 5, 2026

Copy link
Copy Markdown
Owner

initialSettings: called chatWebViewInAppSettings() from inside build(), so every rebuild of the chat surface allocated a fresh InAppWebViewSettings together with the WebViewAssetLoader and AssetsPathHandler it carries. Those are platform-interface objects — constructing one registers it with the plugin, and only the instance handed to the platform view at creation time is ever used, so every later allocation was registered and then dropped without being released.

Changes

  • chat_webview_surface.dart: hold the settings in a late final field built once per surface instead of calling chatWebViewInAppSettings() on every build.
  • chat_webview_preload.dart: same pattern in the preloader, which rebuilt its settings on every build until the preload finished.

Verification

Profile build on a Pixel 8 Pro emulator (Android 16, x86_64) with a seeded 600-message chat, driven over adb; heap sampled through the Dart VM service with getAllocationProfile(gc: true).

  • Before, per 10 chat open/close cycles: AssetsPathHandler +192, +195, +190 (MethodChannel, _Channel, _ChannelCallbackRecord, ListQueue track it one-for-one) — linear over 30 cycles and unchanged across five consecutive forced GCs, so the objects are retained rather than uncollected.
  • After: +10, +10, +10 over the same 30 cycles. Dart heap growth over 10 cycles drops from ~150-410 KB to ~8 KB.
  • flutter analyze — 9 issues, all info/warning, identical to the nightly baseline and none in the touched files.
  • flutter test — 3812 pass. The 4 failures (agent_ops_localization_contract_test.dart, three clipboard cases in chat_input_bar_test.dart) reproduce on unpatched nightly and are local-only: the contract test needs the easy_localization:generate step CI runs, and the clipboard cases pass in CI on ubuntu.

One settings object per surface mount is still never released; that disposal belongs to the plugin side and is left for a follow-up.

🤖 Generated with Claude Code

`initialSettings:` called `chatWebViewInAppSettings()` inside `build()`,
so every rebuild of the chat surface allocated a fresh
`InAppWebViewSettings` — and with it the `WebViewAssetLoader` and
`AssetsPathHandler` it carries. Those are platform-interface objects:
constructing one registers it with the plugin, and only the instance
handed to the platform view at creation time is ever used, so every
later allocation was registered and then dropped without being released.

Measured on a Pixel 8 Pro emulator (profile build, 600-message chat):
each chat open/close cycle leaked ~19 `AssetsPathHandler` /
`MethodChannel` / `ListQueue` objects — linear over 30 cycles and
surviving five forced GCs. Holding the settings in a `late final` field
drops that to 1 per cycle. The preloader had the same pattern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hydall
hydall merged commit 045a994 into nightly Sep 5, 2026
3 checks passed
@hydall
hydall deleted the fix/webview-settings-realloc branch September 5, 2026 13:12
hydall added a commit that referenced this pull request Sep 5, 2026
)

`chatWebViewAssetLoader()` built a fresh `WebViewAssetLoader` — and with
it an `AssetsPathHandler` — on every call. Each handler's `_init`
registers a `MethodChannel` under a unique name and installs a method
call handler on the binary messenger, and nothing ever disposes it.

On Android the chat WebView is kept alive and reused
(`chatWebViewKeepAliveForPlatform`). `FlutterWebViewFactory.create`
returns the cached `FlutterWebView` for a known `keepAliveId` and only
constructs a new one — the sole place `initialSettings` is parsed — when
there is no cached instance. So every mount after the first handed the
native side settings it never reads, while the handler they carried
stayed registered on the Dart side forever.

Disposing per mount would be wrong: whichever mount created the native
WebView owns the live handler, and that is the preloader in practice.
One shared loader sidesteps the ownership question — the native side
only ever uses a single one, and `handle()` is stateless.

Verified on a Pixel 8 Pro emulator (profile build, 600-message chat):
`AssetsPathHandler` and `MethodChannel` now stay flat at 1 and 23 across
30 chat open/close cycles (+0, +0, +0), down from +10 per 10 cycles
after #390 and +190 before it. Dart heap is flat too. The chat renders
with styles, avatars and the Context card intact, and the log carries no
asset-loading errors.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant