Repository: flutter
Issue: [web] Paint and Path memory is not reclaimed fast enough
Labels: c: performance,platform-web,perf: memory,has reproducible steps,P1,team-web,triaged-web,found in release: 3.24,found in release: 3.25

### Steps to reproduce

1. Create new Flutter project: `flutter create bug && cd bug`
2. Add a dependency on `pkg:vector_math`: `flutter pub add vector_math` (dependency on `vector_math` is almost certainly not needed but it makes the repro code cleaner)
3. Replace `lib/main.dart` with the code below (in code sample section).
4. Run in any non-web target (e.g. `flutter run -d macos` or `flutter build macos`) to ensure there is no memory leak (memory stays at a certain level):

![Screenshot 2024-08-18 at 12 17 38](https://github.com/user-attachments/assets/4b578404-a389-47f4-bb52-ebb5565ab563)

6. Run on the web (e.g. `flutter build web`) in any configuration (release / profile / debug, wasm or not, etc.)


### Expected results

Used memory doesn't grow.

### Actual results

Memory used by the web app goes up linearly at several megabytes per second:

![Screenshot 2024-08-18 at 12 57 41](https://github.com/user-attachments/assets/277e8b05-f28d-4232-8d21-a323547d3903)

After a couple of minutes, the memory consumed reaches 1.0 GB and keeps growing:

![Screenshot 2024-08-18 at 12 22 33](https://github.com/user-attachments/assets/f7e23ae1-87a5-4c86-9c58-7a6c313acec2)

I tried debugging this through Dart DevTools but Dart DevTools don't have access to memory info for web apps. All I can do is use Chrome DevTools. I can't say I understand what I'm looking at in Chrome DevTools, but here are some things I noticed:

1. Basically all the RAM is taken by some `JSArrayBufferData` object:

![Screenshot 2024-08-18 at 13 03 55](https://github.com/user-attachments/assets/15d93068-56c0-43d4-a085-8a7f35c478cb)

2. This object seems to have something in common with a `flutterCanvasKit` object, and specifically with something called `globalThis`.

![Screenshot 2024-08-19 at 09 31 43](https://github.com/user-attachments/assets/0162cd16-f2d4-42b4-92e7-e9025f6682e3)

Snapshot file from Chrome DevTools: [Heap-20240818T130707.heapsnapshot.zip](https://github.com/user-attachments/files/16657126/Heap-20240818T130707.heapsnapshot.zip)

I initially thought this had something to do with Flame but as you can see from the sample, the same issue appears on vanilla Flutter as well. // cc @zoeyfan 


### Code sample

<details open><summary>Code sample</summary>

It's possible that the code could be made simpler but before I invest that kind of work, I'd like to make sure this is actually a bug and that I'm not overlooking something obvious or doing something stupid.

```dart
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:vector_math/vector_math_64.dart' hide Colors;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final String title;

  const MyHomePage({super.key, required this.title});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(title),
      ),
      body: const Center(
        child: MemoryPressureWidget(),
      ),
    );
  }
}

class MemoryPressureWidget extends StatefulWidget {
  const MemoryPressureWidget({super.key});

  @override
  State<MemoryPressureWidget> createState() => _MemoryPressureWidgetState();
}

class _MemoryPressureWidgetState extends State<MemoryPressureWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  final List<PairedWanderer> wanderers = [];

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        for (final wanderer in wanderers)
          PairedWandererWidget(wanderer: wanderer),
      ],
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    _createBatch(1000, const Size(500, 500));

    _controller = AnimationController(vsync: this);
  }

  void _createBatch(int batchSize, Size worldSize) {
    assert(batchSize.isEven);
    final random = Random(42);
    for (var i = 0; i < batchSize / 2; i++) {
      final a = PairedWanderer(
        velocity: (Vector2.random() - Vector2.all(0.5))..scale(100),
        worldSize: worldSize,
        position: Vector2(worldSize.width * random.nextDouble(),
            worldSize.height * random.nextDouble()),
      );
      final b = PairedWanderer(
        velocity: (Vector2.random() - Vector2.all(0.5))..scale(100),
        worldSize: worldSize,
        position: Vector2(worldSize.width * random.nextDouble(),
            worldSize.height * random.nextDouble()),
      );
      a.otherWanderer = b;
      b.otherWanderer = a;
      wanderers.add(a);
      wanderers.add(b);
    }
  }
}

class PairedWanderer {
  PairedWanderer? otherWanderer;

  final Vector2 position;

  final Vector2 velocity;

  final Size worldSize;

  PairedWanderer({
    required this.position,
    required this.velocity,
    required this.worldSize,
  });

  void update(double dt) {
    position.addScaled(velocity, dt);
    if (otherWanderer != null) {
      position.addScaled(otherWanderer!.velocity, dt * 0.25);
    }

    if (position.x < 0 && velocity.x < 0) {
      velocity.x = -velocity.x;
    } else if (position.x > worldSize.width && velocity.x > 0) {
      velocity.x = -velocity.x;
    }
    if (position.y < 0 && velocity.y < 0) {
      velocity.y = -velocity.y;
    } else if (position.y > worldSize.height && velocity.y > 0) {
      velocity.y = -velocity.y;
    }
  }
}

class PairedWandererWidget extends StatefulWidget {
  final PairedWanderer wanderer;

  const PairedWandererWidget({required this.wanderer, super.key});

  @override
  State<PairedWandererWidget> createState() => _PairedWandererWidgetState();
}

class _PairedWandererWidgetState extends State<PairedWandererWidget>
    with SingleTickerProviderStateMixin {
  late Ticker _ticker;

  Duration _lastElapsed = Duration.zero;

  @override
  void initState() {
    super.initState();
    _ticker = createTicker(_onTick);
    _ticker.start();
  }

  @override
  void dispose() {
    _ticker.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Positioned(
      left: widget.wanderer.position.x - 128 / 4,
      top: widget.wanderer.position.y - 128 / 4,
      child: const SizedBox(
        width: 8,
        height: 8,
        child: Placeholder(),
      ),
    );
  }

  void _onTick(Duration elapsed) {
    var dt = (elapsed - _lastElapsed).inMicroseconds / 1000000;
    dt = min(dt, 1 / 60);
    widget.wanderer.update(dt);
    _lastElapsed = elapsed;
    setState(() {});
  }
}
```

</details>


### Screenshots or Video

<details open>
<summary>Screenshots / Video demonstration</summary>

The code sample is compatible with Dartpad.

</details>


### Logs

<details open><summary>Logs</summary>

```console
[Paste your logs here]
```

</details>


### Flutter Doctor output

<details open><summary>Doctor output</summary>

```console
flutter doctor -v
[✓] Flutter (Channel stable, 3.24.0, on macOS 14.4.1 23E224 darwin-arm64,
    locale en-US)
    • Flutter version 3.24.0 on channel stable at
      /Users/filiph/fvm/versions/stable
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 80c2e84975 (3 weeks ago), 2024-07-30 23:06:49 +0700
    • Engine revision b8800d88be
    • Dart version 3.5.0
    • DevTools version 2.37.2

[✓] Android toolchain - develop for Android devices (Android SDK version
    35.0.0)
    • Android SDK at /Users/filiph/Library/Android/sdk
    • Platform android-35, build-tools 35.0.0
    • Java binary at: /Applications/Android
      Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build
      17.0.7+0-17.0.7b1000.6-10550314)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.4)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15F31d
    • CocoaPods version 1.15.0

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2023.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build
      17.0.7+0-17.0.7b1000.6-10550314)

[✓] IntelliJ IDEA Ultimate Edition (version 2023.3.2)
    • IntelliJ at /Users/filiph/Applications/IntelliJ IDEA Ultimate.app
    • Flutter plugin version 78.4.2
    • Dart plugin version 233.15271

[✓] VS Code (version 1.87.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.84.0

[✓] Connected device (3 available)
    • macOS (desktop)                 • macos                 • darwin-arm64
      • macOS 14.4.1 23E224 darwin-arm64
    • Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin
      • macOS 14.4.1 23E224 darwin-arm64
    • Chrome (web)                    • chrome                • web-javascript
      • Google Chrome 127.0.6533.120

[✓] Network resources
    • All expected network resources are available.

• No issues found!
```

</details>