Skip to content

feat(presets): add a skia preset for @shopify/react-native-skia - #290

Open
danfry1 wants to merge 3 commits into
mainfrom
feat/skia-preset
Open

danfry1 wants to merge 3 commits into
mainfrom
feat/skia-preset

Conversation

@danfry1

@danfry1 danfry1 commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR adds a skia preset, which is auto-detected when @shopify/react-native-skia is installed and works under both engines. It is the second half of #286, reported by @believer, which asked for Unistyles and Skia presets; Unistyles is #289.

Closes #286.

Why

Importing Skia in a test threw Native Skia Module failed to correctly install JSI Bindings. Skia's own test support loads CanvasKit, Skia compiled to WebAssembly, and mocks the package with Mock(CanvasKit): the JS Skia API backed by real Skia. The preset uses that support directly instead of hand-writing a stand-in.

Changes

  • src/presets/skia.ts
    • prepare() loads CanvasKit from Skia's own dependency, compiling it once per worker.
    • The module is Skia's Mock(), plus Skia's Reanimated helpers. The real package exports those helpers but the mock omits them.
    • matchFont() returns a font: over CanvasKit, which has no system fonts, the real one throws.
  • src/presets/skia-exports.ts holds the named exports: the factory's names that the real package exports. The mock's leftovers from earlier releases are kept off the named surface.
  • Preset.prepare?: () => Promise<void>
    • The native setup awaits it.
    • The mock-engine setup can't await, because it is also built as CommonJS. Instead it builds the module when preparation settles, and the plugin's virtual module awaits that.
    • A require() before then throws PRESET_NOT_PREPARED, naming the fix (src/preset-preparation.ts).
  • Tests
    • tests-native/skia.test.tsx and tests/skia-preset.test.tsx cover real Skia geometry, Canvas rendering, matchFont, a Reanimated helper, and export-list sync.
    • A setup-file probe (tests-native/support/preset-readiness.setup.ts) records whether CanvasKit loaded before user setup files ran. Without it, removing the native await went unnoticed, because CanvasKit usually finishes first anyway.
  • Dependencies: @shopify/react-native-skia@2.14.0 is a devDependency. Root overrides replace its four prebuilt native binary packages (about 720 MB, read only by the native builds) with scripts/native-binary-stub.
  • Docs: a Skia section in the presets guide, rows in the three preset tables, and a changeset.

Notes

  • Cost. CanvasKit loads before each test file, as Skia's Jest environment does: about 9 ms to compile and 14 ms to initialise per file, single-threaded, and more under parallel load. This applies only to projects with Skia installed, and presets: { skia: false } turns it off. I considered preparing only when a file imports Skia, but a CommonJS library that require()s Skia couldn't wait for it, so I rejected that.
  • Synchronous CanvasKit initialisation was measured and rejected: most of the API is attached only after the asynchronous runtime start.
  • Public API additions: presets.skia and the optional Preset.prepare.
  • Before merge:

Skia draws through a JSI binding to its C++ that throws at import in
Node. Skia's own test support (jestEnv.js + jestSetup.js) loads
CanvasKit, Skia compiled to WebAssembly, and mocks the package with
lib/module/mock's Mock(CanvasKit): the JS Skia API backed by real Skia,
with Canvas and SkiaPictureView as React Native Views. The preset does
the same, under both engines, without configuration.

- prepare() loads CanvasKit from the installed Skia's own dependency
  (an isolated layout does not hoist canvaskit-wasm), compiling the
  WebAssembly once per worker. Synchronous instantiation was measured
  and rejected: Emscripten attaches most of CanvasKit's API only when
  its runtime finishes initialising, after the synchronous part.
- The module is Skia's Mock() plus Skia's Reanimated helpers
  (external/reanimated), which the real package exports and the mock
  omits, and a matchFont() that returns a font: over CanvasKit the
  real one throws for want of system fonts.
- Named exports are the factory's names that the real package exports;
  the mock's leftovers from earlier releases stay off the named surface.

Preset gains an optional async prepare(). The native setup awaits it.
The mock-engine setup is also built as CommonJS and cannot await, so
it builds a prepared preset's modules when preparation settles, the
virtual module awaits that, and require() before then gets a
PRESET_NOT_PREPARED error naming the fix rather than the real package's
JSI failure.

@shopify/react-native-skia 2.14.0 is a devDependency; root overrides
replace its four prebuilt native binary packages (about 720 MB, read
only by iOS/Android builds) with an empty stub.
The skia preset's module needs CanvasKit, which prepare() loads, and
its graph reaches CanvasKit's WebAssembly heap: the walker now skips
binary data, whose keys are millions of indices.
…nd runtimes

The forks and hot configs listed their own setup files and left out the
preset-readiness probe, so skia.test.tsx read undefined there. All three configs
that run tests-native/*.test.ts{,x} now take one list.
@believer

believer commented Oct 11, 2026 •

Copy link
Copy Markdown

As of version >=2.15.0 and 3, the package name is react-native-skia following Shopify's native migration

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.

Unistyles and Skia presets

2 participants