Skip to content

feat(unplugin-typegpu): Experimental unstable_stripJsImpl - #3247

Draft
iwoplaza wants to merge 1 commit into
impr/metadata-v3from
feat/strip-js-impl
Draft

iwoplaza wants to merge 1 commit into
impr/metadata-v3from
feat/strip-js-impl

Conversation

@iwoplaza

@iwoplaza iwoplaza commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Every 'use gpu' function ships twice: as its JS implementation, and as its AST in the metadata. The JS implementation is only needed when the function is called on the CPU, which most of them never are.

With unstable_stripJsImpl: true, the plugin replaces the implementation with a stub. The first time the stub is called, it recreates the function from the AST.

__tgpu_meta(function __tgpu_stub(a0, a1, ...r) {
  return (__tgpu_stub.j ??= __tgpu_revive(__tgpu_stub))(a0, a1, ...r);
}, '[["a","b"],[[10,[1,"a","+","b"]]]]', {}, "add")
  • reviveFunction (in typegpu/~internal) prints the AST back to JS with tseynit, rewrites + - * / % into __tsover_* calls like the plugin does, and builds the function with new Function. Externals that are property paths (config.nested.offset) become nested objects.
  • The plugin imports reviveFunction only into modules that contain stubs, so apps that don't enable the option pay nothing.
  • The stub keeps the original .length. TypeGPU relies on it: createGuardedComputePipeline infers its dimensionality from callback.length. An earlier prototype with a (...args) stub silently changed jelly-slider's compute shader from @workgroup_size(16, 16, 1) to (1, 1, 1).
  • Stubs have no 'use gpu' directive, so later plugin passes leave them alone.

Caveats, which is why it's opt-in and unstable_:

  • new Function requires a CSP that allows 'unsafe-eval'.
  • Externals are read on the first call, not on every call.
  • The recreated function has no source mapping.

Measurements

Same four apps and setup as #3246 (lab in #3248). Compared against #3246 with the option off (bytes, minified / gzip / brotli):

App Off On Change
monocular-light-injection 505,710 / 124,788 / 104,119 473,574 / 115,910 / 96,707 −32,136 / −8,878 (−7.1%) / −7,412
selfie-segmentation 359,922 / 96,240 / 80,688 353,684 / 93,896 / 78,743 −6,238 / −2,344 (−2.4%) / −1,945
os-awards 459,476 / 124,605 / 104,049 446,767 / 119,392 / 99,585 −12,709 / −5,213 (−4.2%) / −4,464
jelly-slider 429,710 / 114,127 / 95,538 416,463 / 108,729 / 90,883 −13,247 / −5,398 (−4.7%) / −4,655

Together with unstable_obfuscate + unstable_minify, gzip drops by another 2,356–8,900 B compared to those options alone. The compressed gains are much larger than #3246's, because the removed JS is information the AST already holds, rather than syntax gzip can compress.

With the option off, bundles are byte-identical to #3246's.

Checks:

  • A test app calls 'use gpu' functions on the CPU (vector operators, dotted externals, destructured params, loops) and prints their WGSL. The output is identical with and without the option, also with obfuscation.
  • In Chromium, the four apps' shader modules are byte-identical to main's.

Replaces the JS implementation of 'use gpu' functions with a stub that keeps
the original arity. On the first CPU call, the stub recreates the function from
its AST with `reviveFunction` (typegpu/~internal), using `new Function`.
@iwoplaza
iwoplaza added this pull request to stack #3249 October 21, 2026 10:12
@iwoplaza iwoplaza changed the title feat(unplugin-typegpu): Experimental unstable_stripJsImpl option feat(unplugin-typegpu): Experimental unstable_stripJsImpl Oct 21, 2026
@github-actions

Copy link
Copy Markdown

pkg.pr.new

packages
Ready to be installed by your favorite package manager ⬇️

https://pkg.pr.new/software-mansion/TypeGPU/tinyest@af321c64c894de80cad0342a6b507129ca519be3
https://pkg.pr.new/software-mansion/TypeGPU/typegpu@af321c64c894de80cad0342a6b507129ca519be3
https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@af321c64c894de80cad0342a6b507129ca519be3

benchmark
view benchmark

commit
view commit

@github-actions

Copy link
Copy Markdown

Resolution Time Benchmark

---
config:
  themeVariables:
    xyChart:
      plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
  title "Random Branching (🔴 PR | 🔵 main | 🟢 release)"
  x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
  y-axis "time (ms)"
  line [1.02, 2.04, 4.26, 6.91, 7.73, 12.34, 26.03, 25.71]
  line [1.01, 2.11, 4.57, 6.79, 8.12, 13.43, 26.98, 27.17]
  line [0.93, 1.90, 4.18, 6.75, 7.70, 12.85, 26.06, 26.00]
Loading
---
config:
  themeVariables:
    xyChart:
      plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
  title "Linear Recursion (🔴 PR | 🔵 main | 🟢 release)"
  x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
  y-axis "time (ms)"
  line [0.35, 0.59, 0.74, 0.88, 1.25, 1.26, 1.60, 1.76]
  line [0.33, 0.54, 0.74, 0.95, 1.21, 1.26, 1.51, 1.67]
  line [0.27, 0.50, 0.72, 0.87, 1.21, 1.19, 1.41, 1.57]
Loading
---
config:
  themeVariables:
    xyChart:
      plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
  title "Full Tree (🔴 PR | 🔵 main | 🟢 release)"
  x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
  y-axis "time (ms)"
  line [0.82, 2.23, 3.53, 6.98, 13.68, 29.17, 57.82, 121.85]
  line [0.98, 2.30, 3.77, 7.13, 13.52, 28.55, 61.65, 122.73]
  line [0.82, 2.10, 3.43, 7.05, 12.85, 26.73, 56.49, 113.94]
Loading

@github-actions

Copy link
Copy Markdown

Bundle size comparison (import * as ... in PR vs import * as ... in target):

🟢 Decreased ➖ Unchanged 🔴 Increased ❔ Unknown
0 352 0 0

No notable changes.

If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu.

This branch has not been deployed

No deployments
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.

(RFC) An additional minify tier that reconstructs CPU functionality on demand using tseynit

1 participant