Skip to content

Lower the memory of Compose, Flutter, Flet, Gio, Tauri, Dioxus, Wails and Capacitor - #22

Merged
linghengqian merged 11 commits into
mainfrom
quick-heron
Oct 24, 2026
Merged

linghengqian merged 11 commits into
mainfrom
quick-heron

Conversation

@linghengqian

@linghengqian linghengqian commented Oct 23, 2026 •

Copy link
Copy Markdown
Member
Implementation Before After
Compose 253 129
Gio 200 165
Flutter 339 172
Dioxus 261 203
Tauri 262 208
Flet about 340 216
Wails 341 223
Capacitor 305 267
  • One commit per implementation, then docs/memory.md. What they do,
    • Flutter and Flet draw with Skia on the processor (FLUTTER_LINUX_RENDERER=software, and Impeller off in Flutter 3.47, which stops on its own text otherwise). Flutter also uses a tenth of the processor time until the Home page is shown.
    • Compose draws with Skia on the processor and uses the C1 compiler of the JIT only, about 50 MB each.
    • Tauri, Dioxus and Wails turn off the DMA-BUF renderer and the JIT of WebKit, Wails also keeps GTK 4 from opening OpenGL, which was most of its 71 MB over Tauri.
    • Capacitor keeps the GPU and the network service in the main process (six processes become four) and runs V8 in lite mode.
    • Gio holds 20 MB in its heap of Go instead of 57, with one parse of the variable font instead of three and a collector at 25 %.
  • check and e2e pass for all eight, and the screenshots of Flutter. On a headless sway Tauri, Wails and Flutter draw the pages as before.
  • Not tested, a real GPU, GNOME, the bubblewrap sandbox of WebKitGTK (the variables are inherited by it according to its source) and the frame rate of the processor renderers on a large or HiDPI window.
  • Gio still draws with OpenGL, and the engine of Flutter and the processes of WebKit still create OpenGL contexts, so Mesa's compiler is in their numbers. On a machine with a GPU they will be different.
  • flet build keeps the project it generated from the template, so a cache of an older build has to be deleted for the patched template to take effect, the README says so.

Without a real GPU, the OpenGL that Impeller draws with is Mesa's
software rasterizer and its compiler, which take as much memory as the
rest of the application. The Linux embedder of Flutter 3.47 draws with
Skia on the processor when FLUTTER_LINUX_RENDERER=software, and Impeller
has to be off for that: the engine stops with "Impeller DlText cannot be
drawn to a Skia canvas" otherwise, which my_application.cc prevents with
fl_dart_project_set_enable_impeller. main.cc sets the variable, and
GDK_GL=disable so that GDK opens no OpenGL for its windows on Wayland,
before GTK starts, unless they are already set.

Takes the Home page from 339 to 172 MB of PSS, and the processor time
until it is shown from 4 to 5 seconds to 0.6. The engine still creates
its OpenGL contexts when it starts (fl_opengl_manager_new in
fl_engine.cc), whatever it draws with, so Mesa's compiler is part of the
172 MB. check, e2e and screenshots still pass, and the pages look the
same. Measured again with sandbox/sandbox.sh measure.
The Flutter engine of Flet 1.0 (3.44) takes FLUTTER_LINUX_RENDERER=software
too, and does not need more for it: it does not stop on the text of
Impeller as the engine of 3.47 does. tools/patch_build_template.py,
which patches the cached template of flet build for the size of the
window, now has main.cc of the template set FLUTTER_LINUX_RENDERER=software
and GDK_GL=disable before GTK starts, unless they are already set, as the
Flutter implementation does. It checks each patched file on its own, and
sees that one is already patched by the new text, not by the absence of
the old.

Takes the Home page from about 340 to 216 MB of PSS (the same build with
OpenGL: 332 to 356 MB in three runs). flet build keeps the project that
it generated from the template, so a cache of an older build, such as
the vfox-plus-sandbox-flet-build volume of the sandbox, has to be
deleted for a changed template to take effect; the README says so.
check and e2e still pass. Measured again with sandbox/sandbox.sh
measure.
Skiko draws with OpenGL by default, which maps Mesa's software OpenGL
and its compiler when there is no real GPU, and the optimizing compiler
of the JIT (C2) keeps tens of megabytes of arenas of its own.
-Dskiko.renderApi=SOFTWARE has Skia draw on the processor and
-XX:TieredStopAtLevel=1 leaves C2 out. Each takes about 50 MB of PSS off
the Home page, 253 to 129 MB together, and the processor time until the
page is shown halves. The pages look the same.

Heap sizes, the class data sharing archive that the jlink runtime of
jpackage lacks, and the size of the code cache and of the stacks made
no difference, or a worse one. check and e2e still pass (the end-to-end
tests run in the test window of Compose, not with these arguments).
Measured again with sandbox/sandbox.sh measure.
On top of WEBKIT_DISABLE_COMPOSITING_MODE (#20):
WEBKIT_DISABLE_DMABUF_RENDERER=1 keeps WebKit from sharing buffers with
the GPU, which maps Mesa's software OpenGL and its compiler in the
processes of the web view when there is no real GPU, and
JSC_useJIT=false leaves out the JIT compilers of the JavaScript engine,
which have memory of their own in the web process. The pages of vfox+
need neither: the versions page opens as fast. GDK_GL=disable keeps GDK
from opening OpenGL for its windows on Wayland (Tauri on a headless
sway: 242 MB of PSS without it, 207 with it; no difference on X11). All
four are set in lib.rs before the webview starts, unless they are
already set.

Takes the Home page from 262 to 208 MB of PSS; check and both e2e
scenarios (fake and real) still pass. Measured again with
sandbox/sandbox.sh measure.
Same as Tauri: WEBKIT_DISABLE_DMABUF_RENDERER=1, JSC_useJIT=false and
GDK_GL=disable, with WEBKIT_DISABLE_COMPOSITING_MODE=1, set in main.rs
before the webview starts, unless they are already set.

Takes the Home page from 261 to 203 MB of PSS; check and both e2e
scenarios (fake and real) still pass. Measured again with
sandbox/sandbox.sh measure.
WEBKIT_DISABLE_DMABUF_RENDERER=1 and JSC_useJIT=false, as in Tauri and
Dioxus. GTK 4 also draws its own window with OpenGL (its ngl renderer),
which maps Mesa's software OpenGL and its compiler in the application
when there is no real GPU: GDK_DISABLE=gl,vulkan keeps GDK from opening
OpenGL and Vulkan, and GSK_RENDERER=cairo has GTK draw with Cairo. That
was most of what WebKitGTK 6.0 with GTK 4 needed more than WebKitGTK 4.1
with GTK 3: Wails is 15 MB above Tauri now, not 71 (building with the
gtk3 tag, the legacy path of Wails 3, would save 9 MB more). All five
variables are set in main.go before the application starts, unless they
are already set.

Takes the Home page from 341 to 223 MB of PSS; check and both e2e
scenarios (fake and real) still pass, and on a headless sway the pages
are drawn as before. Measured again with sandbox/sandbox.sh measure.
On top of app.disableHardwareAcceleration() (#20): in-process-gpu puts
the GPU thread in the browser process, NetworkServiceInProcess2 does the
same for the network service, and --js-flags=--lite-mode has V8 leave out
its optimizing compiler. Six processes become four, and the Home page
goes from 305 to 267 MB of PSS. --no-zygote saved nothing in the test
and needs the sandbox of Chromium off, and --single-process does not
start the application.

The README no longer says that the web view implementations cost more
than this one: with the settings of Tauri, Dioxus and Wails they are
203 to 223 MB. The experiment that hosted this front end in the shell of
Tauri (422 MB) predates them, and is marked so. check and both e2e
scenarios (fake and real) still pass. Measured again with
sandbox/sandbox.sh measure.
The heap of Go was 57 MB for 22 MB that were live, 12 of them copies of
the same tables. Every one of the three weights of the variable font of
the interface parsed the font again, because Gio tells faces apart by
their *Font. They now get a Font of their own that is a copy of the one
parsed once, which shares its tables, and main.go has the collector run
at 25 % growth instead of 100 %, so that the heap stays near what the
application holds (10 to 12 MB).

The heap of Go goes from 57 to 20 MB, and the Home page from 200 to
165 MB of PSS, in a number that varies by 20 MB from one run to the next
with Mesa's llvmpipe, which is most of it: Gio only draws with OpenGL.
check and e2e still pass, and the pages look the same. Measured again
with sandbox/sandbox.sh measure.
docs/memory.md has the numbers of the commits before, and of mygo and
Uno, which did not change (mygo 41 MB of PSS on the Home page, Uno 98),
measured on the same day, one after another, with nothing else running
(sandbox/sandbox.sh measure of each): Compose 129, Gio 165, Flutter 172,
Dioxus 203, Tauri 208, Flet 216, Wails 223 and Capacitor 267. The same
code before the commits took 253, 200, 339, 261, 262, about 340, 341 and
305. mygo is still the lowest by far, now by a factor of 2.4 to 6.5.

The Results bullets and the No GPU paragraph say what each
implementation did and what is left: Gio still draws with OpenGL, the
engine of Flutter and the processes of WebKit still create OpenGL
contexts, and on Wayland GDK opens OpenGL for the windows of GTK 3
unless told not to. Uno's README has its own numbers of the day (98 and
113 MB); the others are in their own commits.
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Memory use at b5c9663

  • Changed from main beyond the noise of CI: Compose: Home PSS -128 MB (-50%), Versions PSS -116 MB (-43%); Flutter: Home PSS -159 MB (-50%), Versions PSS -159 MB (-49%); Flet: Home PSS -87 MB (-29%), Versions PSS -107 MB (-33%); Dioxus: Home PSS -47 MB (-17%), Versions PSS -61 MB (-20%); Tauri: Home PSS -51 MB (-18%), Versions PSS -56 MB (-18%); Wails: Home PSS -103 MB (-29%), Versions PSS -101 MB (-27%); Capacitor: Home PSS -24 MB (-7%), Versions PSS -23 MB (-7%), processes 6 -> 4.
  • Within the noise of CI: mygo, Uno, Gio.
Implementation Technology Processes Home PSS Home RSS Versions PSS Versions RSS The list adds (PSS)
mygo mygo 0.5, Go, GTK 3 (purego), CPU renderer 1 42 (0) 56 50 (0) 65 +8
Uno Uno Platform 6, C# on .NET 10, Skia on the CPU, Native AOT 1 101 (0) 109 127 (-1) 135 +26
Compose Compose Multiplatform, Kotlin on the JVM, Skia 1 129 (-128) 134 152 (-116) 158 +23
Flutter Flutter 3.47, Dart, Impeller (OpenGL ES), GTK 3 4 162 (-159) 207 167 (-159) 212 +5
Gio Gio, Go, OpenGL ES through EGL 1 172 (-21) 197 178 (-18) 203 +6
Flet Flet, Python 3.14 in a Flutter application 4 214 (-87) 253 222 (-107) 260 +8
Dioxus Dioxus 0.7, Rust, WebKitGTK, compositing off 3 227 (-47) 427 244 (-61) 445 +17
Tauri Tauri 2, Rust, WebKitGTK 4.1 (GTK 3), compositing off 3 235 (-51) 408 252 (-56) 426 +17
Wails Wails 3, Go, WebKitGTK 6.0 (GTK 4), compositing off 3 250 (-103) 422 273 (-101) 437 +23
Capacitor Ionic Vue and Capacitor in Electron, no hardware GPU 4 (-2) 299 (-24) 607 308 (-23) 620 +9

In MB, for the application and all of its child processes, sorted by the PSS of the Home page; "Versions" is the SDKs page with the available versions of Node.js listed. A number in parentheses is the difference from main, in bold when it is more than the noise of CI. "(main)" marks an implementation that this pull request did not rebuild: its numbers are those of main.

Details
  • Measured by shared/tools/measure.sh (Flutter: flutter/tool/measure.py) in the workflow of each implementation, on a GitHub-hosted runner with Xvfb and no GPU. These are not the numbers of docs/memory.md, which come from another machine: compare the rows of one comment.
  • The same code does not use the same memory in every CI run, so a difference counts as a change only when it is more than 5 MB and more than 5% of the number on main (8% for Compose and Flet, 12% for Gio, 15% for Flutter, which vary more).
  • Runs of this pull request: mygo, Uno, Compose, Flutter, Gio, Flet, Dioxus, Tauri, Wails, Capacitor.
  • Latest runs on main: mygo (79ef675), Uno (79ef675), Compose (79ef675), Flutter (79ef675), Gio (79ef675), Flet (79ef675), Dioxus (79ef675), Tauri (79ef675), Wails (79ef675), Capacitor (79ef675).

@linghengqian
linghengqian marked this pull request as draft October 23, 2026 14:03
On 2026-10-23 the Ubuntu mirror of the runners was throttled to about
40 KB/s: the index (3 MB) still came at 10 MB/s, but the 280 packages
of the Wails workflow, 94 MB, took 45 minutes, the timeout of the job,
where the same step took a minute on 2026-10-19. The runs of #22 show
it: Wails was cancelled after 45 minutes in "Install the Linux
packages", the packaging job of Flutter hit its 25 minutes in the same
kind of step, in two attempts, and Uno (36 minutes), Dioxus (30) and
Capacitor (21) spent most of their time there. Nothing else of those
jobs was slow.

The throttle seems to be for each connection: the rate stayed at about
40 KB/s from 12:23 to 12:38 UTC while the number of jobs that were
downloading fell from eleven to five. apt downloads from one host over
one connection, one file after the other.

The workflows install their packages with the new action
.github/actions/install-linux-packages:

* It keeps the downloaded archives in a cache, for each list of packages
  and each week, so that a day with a slow mirror costs the first run
  only. The newest cache of any earlier week starts the next one, and
  apt-get autoclean removes the archives of superseded versions.
* install.sh downloads what the cache lacks eight files at a time with
  curl (fetch.sh, from the addresses that apt prints with --print-uris,
  with the address of the mirror file of the runners filled in), then has
  apt fetch what is left and check every archive, and installs from the
  archives, which needs no network and no time limit. A download has a
  time limit of five minutes and three attempts, and keeps what it got.
  A request that gets no data for 30 seconds fails and is tried again, up
  to three times; apt waited for two minutes. Where the files cannot be
  fetched this way, apt does all of it, as before.
* A run that cannot finish saves what it downloaded under another key,
  so that the next run, such as a re-run of the failed jobs, starts from
  there and gets further each time.

The job that runs the end-to-end tests of Flutter no longer installs
patchelf, lintian and desktop-file-utils, which only the job that builds
the packages uses. Dependabot also looks at the action, which pins
actions too.

Tested in an ubuntu:26.04 container with the mirror of this network and
a mirror file like that of the runners: a cold run, a run from the saved
archives (0 B to download), eight connections against one when curl is
limited to 150 kB/s each (all of 164 MB by curl in about two and a half
minutes, where one connection ran into the time limit after 47 MB),
archive names with an epoch (%3a), an attempt that ran out of time, and
an installation that fails after its last attempt; the package list of
the end-to-end job of Flutter installs its toolchain. actionlint 1.7.12
and ShellCheck 0.11 pass. Not tested on a runner: the cache steps, and
whether the throttle is for each connection.
The Wails job of #22 failed with the first version of the action (run
39971828235): "Could not download the packages in 3 attempts of 5m"
after 30 minutes, and nothing was saved. The log shows what went wrong:

* fetch.sh wrote each file from its first byte, over what the attempt
  before it had got. libwebkitgtk-6.0-4 is 25.5 MB, 10 minutes at the
  43 KB/s that apt got over one connection of that runner, and an
  attempt had 5, so it could not end however often it was tried. The
  mirror also cut a connection after a few minutes, which nothing
  continued.
* Eight connections got 138 KB/s together (41.5 MB in 5 minutes), not
  eight times 43 KB/s, and four downloads did not connect in 10 seconds.
  In the same minute other jobs downloaded 90 MB from the same mirror in
  seconds: only some runners are slow.
* The save of what had been downloaded failed, because tar cannot read
  an archive that the user of the runner has no right to (libkdb5-10t64,
  one of 280), so that the next run would have started from nothing.

fetch.sh now:

* continues what is in partial/ with a range request, so that a
  connection that is cut, or dropped, loses nothing;
* takes the mirrors of /etc/apt/apt-mirrors.txt in order, drops a
  connection that gets less than 256 KB/s for 10 seconds, and goes on
  with the next mirror. When every mirror was slow, it goes on with the
  best of them and drops only a connection that stalls. A file that no
  mirror sends in three rounds is given up;
* ends at one deadline for the whole download, 15 minutes, instead of
  attempts of 5 minutes that each started over. install.sh starts the
  largest files first, so that none is left alone at the end;
* checks the SHA256 hash that apt prints (-o Acquire::ForceHash=SHA256)
  before it moves an archive into the directory. apt takes an archive of
  the right size that is there for a good one and does not check it: in
  a test, dpkg failed on one with a changed byte. The archives that are
  there already, from the cache, are checked too, and replaced when they
  are wrong. apt prints only the archives that are missing, so install.sh
  shows it an empty directory to get all of them.

The action makes the archives readable before it saves the cache, also
when the installation failed.

.github/scripts/test_fetch.py runs fetch.sh against local mirrors that
are slow, cut their connections, have nothing or send the wrong file; the
step of repository.yml that discovers the tests of that directory runs it
(15 seconds). In an ubuntu:26.04 container with a file of mirrors like
that of the runners, whose first mirror does not answer: a cold run, a
warm run, an archive with a changed byte, a download that ends at its
deadline and is continued by the next run, and no file of mirrors.
actionlint, ShellCheck and ruff pass. Not tested on a runner: the restore
of the cache, and the speed of the other mirrors when the first is slow.
@linghengqian
linghengqian marked this pull request as ready for review October 24, 2026 00:59
@linghengqian
linghengqian merged commit 20170ec into main Oct 24, 2026
24 checks passed
@linghengqian
linghengqian deleted the quick-heron branch October 24, 2026 00:59
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