Skip to content

feat(shiki): use wasm engine #7896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 27, 2025
Merged

feat(shiki): use wasm engine #7896

merged 2 commits into from
Jun 27, 2025

Conversation

avivkeller
Copy link
Member

@avivkeller avivkeller commented Jun 25, 2025

Using the WASM shiki on the server, and the JavaScript shiki on the client, should result in the best mix of speed and size for our use-case.

See nodejs/api-docs-tooling#320.

Rouhgly a 50% speed improvement on the server, per my testing.

@Copilot Copilot AI review requested due to automatic review settings June 25, 2025 15:27
@avivkeller avivkeller requested review from a team as code owners June 25, 2025 15:27
Copy link

vercel bot commented Jun 25, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
nodejs-org ✅ Ready (Inspect) Visit Preview Jun 26, 2025 7:34pm

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new approach to syntax highlighting by using a WASM-based engine on the server and a JavaScript-based engine on the web, optimizing for speed and bundle size.

  • Minimal web highlighter now explicitly uses the JavaScript regex engine.
  • Server highlighter is updated to initialize and use the WASM Oniguruma engine with top-level await.
  • ESLint configuration is updated to support top-level await by using the latest ECMAScript version.

Reviewed Changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/rehype-shiki/src/minimal.mjs Added explicit engine selection for the minimal (web) highlighter
packages/rehype-shiki/src/index.mjs Integrated asynchronous WASM engine initialization for the server side
packages/rehype-shiki/src/highlighter.mjs Removed the default engine creation to allow external engine injection
packages/rehype-shiki/package.json Added dependency for the WASM Oniguruma engine
packages/rehype-shiki/eslint.config.js Updated ECMAScript version to support top-level await
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

packages/rehype-shiki/src/highlighter.mjs:17

  • Since the default engine creation was removed, it may be helpful to document that an engine must now be provided via options when calling createHighlighter.
  const shiki = createHighlighterCoreSync({

Copy link
Member

@AugustinMauroy AugustinMauroy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's super !

@avivkeller
Copy link
Member Author

avivkeller commented Jun 25, 2025

@dario-piotrowicz https://github.com/nodejs/nodejs.org/actions/runs/15880572422/job/44779523238?pr=7896

Does OpenNext support WASM?

@dario-piotrowicz
Copy link
Member

dario-piotrowicz commented Jun 25, 2025

@dario-piotrowicz https://github.com/nodejs/nodejs.org/actions/runs/15880572422/job/44779523238?pr=7896

Does OpenNext support WASM?

sorry, not sure... this code comment suggests that it should work... I'll check with @vicb tomorrow, but I guess that we might be hitting some open-next bug here 🤔

@vicb
Copy link

vicb commented Jun 26, 2025

@dario-piotrowicz https://github.com/nodejs/nodejs.org/actions/runs/15880572422/job/44779523238?pr=7896
Does OpenNext support WASM?

sorry, not sure... this code comment suggests that it should work... I'll check with @vicb tomorrow, but I guess that we might be hitting some open-next bug here 🤔

It looks like the code is trying to create the module from a UInt8Array:

image

That is not supported on Workers - they need the wasm to be loaded from a .wasm. Similar to why eval(...) doesn't work.

@ovflowd
Copy link
Member

ovflowd commented Jun 26, 2025

Can we store the Uint8Array as a file? The contents of it, or is there some way we can convert this to a wasm file. My understanding is we can pass the wasm instance to Shiki and we don't need to import this module....

@vicb
Copy link

vicb commented Jun 26, 2025

Can we store the Uint8Array as a file? The contents of it, or is there some way we can convert this to a wasm file. My understanding is we can pass the wasm instance to Shiki and we don't need to import this module....

It should be a .wasm file.
I guess the Uint8Array is generated from some .wasm file so that file should be used instead - it doesn't look like it's part of @shikijs/engine-oniguruma. Maybe then a PR upstream.

@vicb
Copy link

vicb commented Jun 26, 2025

Looking at the code it looks like the wasm file is in vscode-oniguruma - shiki uses v1.7.0

@avivkeller
Copy link
Member Author

avivkeller commented Jun 26, 2025

@vicb is there any way OpenNext can support loading WASM files via WebAssembly.instantiate-ing the content, like is done here?

 CompileError: WebAssembly.instantiate(): Wasm code generation disallowed by embedder

Shiki supplies custom imports (env and wasi_snapshot_preview1) via a custom importObject passed to WebAssembly.instantiate.

Hypothetically, we might be able to do some custom import resolving, but that's a lot of boilerplate. We could also conditionally use the JavaScript engine in OpenNext, and the WASM one otherwise? Does OpenNext have any specific environment variable we can use to identify when it's in use?

Copy link

codecov bot commented Jun 26, 2025

Codecov Report

Attention: Patch coverage is 11.76471% with 15 lines in your changes missing coverage. Please review.

Project coverage is 75.31%. Comparing base (9abc4fe) to head (a8899cf).
Report is 5 commits behind head on main.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/rehype-shiki/src/index.mjs 11.76% 15 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7896      +/-   ##
==========================================
- Coverage   75.45%   75.31%   -0.14%     
==========================================
  Files         101      101              
  Lines        8311     8326      +15     
  Branches      218      218              
==========================================
  Hits         6271     6271              
- Misses       2038     2053      +15     
  Partials        2        2              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@avivkeller
Copy link
Member Author

Ah, I see cloudflare/cloudflare-docs#13469 documents that this is not allowed. Is there a way to enable it on our end, knowing the security concerns?

If not, I think the conditional JS Engine is the way to go.

@vicb
Copy link

vicb commented Jun 26, 2025

@vicb is there any way OpenNext can support loading WASM files via WebAssembly.instantiate-ing the content, like is done here?

Yes you can WebAssembly.instantiate(module) (a .wasm file uploaded along your source code), just not WebAssembly.instantiate(buffer)

Ah, I see cloudflare/cloudflare-docs#13469 documents that this is not allowed. Is there a way to enable it on our end, knowing the security concerns?

The security concerns are for Cloudflare too - that's why there is no way to instantiate from a buffer.
But instantiating from a module should work.
Using the JS engine is a good solution as well.

@avivkeller
Copy link
Member Author

avivkeller commented Jun 26, 2025

Thank you for all your help! I'll investigate this!

@vicb
Copy link

vicb commented Jun 26, 2025

Thank you for all your help! I'll investigate this!

I pointed to the .wasm file to use in an earlier comment

@avivkeller
Copy link
Member Author

avivkeller commented Jun 26, 2025

In case there isn't a viable solution (which, hopefully, it seems there is), is there a way to tell when our code in running in Cloudflare, versus everything else, so we can go with the conditional JS engine?

@vicb
Copy link

vicb commented Jun 26, 2025

In case there isn't a viable solution (which, hopefully, it seems there is), is there a way to tell when our code in running in Cloudflare, versus everything else, so we can go with the conditional JS engine?

You can use "Cloudflare" in globalThis for testing purpose as a runtime check.
If you go down that road, we could define process.env.OPEN_NEXT_CLOUDFLARE=1 so that the code can be tree shaken during the build.

@avivkeller
Copy link
Member Author

Thank you again for your help! We will, unfortunately, need to conditionally use the JavaScript RegExp engine when we are on Cloudflare Workers.

If we were to import the WASM file like:

// Also available at "vscode-oniguruma/release/onig.wasm"
import wasm from "shiki/onig.wasm";
await import("shiki/onig.wasm");

It would result in errors like, since the imports need to be defined before we import the WASM file:

Module not found: Can't resolve 'wasi_snapshot_preview1'
Module not found: Can't resolve 'env'

Copy link
Member

@ovflowd ovflowd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@avivkeller avivkeller added the github_actions:pull-request Trigger Pull Request Checks label Jun 27, 2025
@github-actions github-actions bot removed the github_actions:pull-request Trigger Pull Request Checks label Jun 27, 2025
Copy link
Contributor

github-actions bot commented Jun 27, 2025

Lighthouse Results

URL Performance Accessibility Best Practices SEO Report
/en 🟢 97 🟢 100 🟢 100 🟢 91 🔗
/en/about 🟢 100 🟢 96 🟢 100 🟠 82 🔗
/en/about/previous-releases 🟢 99 🟢 96 🟢 100 🟠 83 🔗
/en/download 🟢 98 🟢 100 🟢 100 🟢 91 🔗
/en/blog 🟢 100 🟢 100 🟢 96 🟢 92 🔗

@avivkeller avivkeller added this pull request to the merge queue Jun 27, 2025
Merged via the queue into main with commit 6dcda98 Jun 27, 2025
15 checks passed
@avivkeller avivkeller deleted the feat/shiki-wasm branch June 27, 2025 18:37
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.

5 participants