Release Notes *3.2.0*
docker run -it \
-v ~/wljs:"/home/wljs/WLJS Notebooks" \
-v ~/wljs/Licensing:/home/wljs/.WolframEngine/Licensing \
-v ~/wljs/tmp:/tmp \
-p 8000:3000 \
--name wljs \
ghcr.io/wljsteam/wljs-notebook:3.2.0brew install --cask wljs-notebookSummary
- Zero-lag saving
- Improved mixed-content rendering for equations and objects
- 8× faster initial kernel launch
- Faster exports, 2× faster 2D and 3D graphics, and faster audio streaming
- 7400× faster JavaScript-to-WL pipeline (you can literally stream raw video directly from JavaScript to Wolfram)
- Improved frontend garbage collector
- Better Dialog API
- Safer
Refreshand automatic throttling forAnimate - Improved export of static and interactive notebooks to HTML
- Improved Mathematica notebook imports
- Access to server logs from the app and kernel crash detection
- Caching improvements
- Fixed race-condition bugs in the MCP server
- Better internal architecture, fully refactored in three passes
- Improved export options
- Better React/Web Components integration
- More integration tests (> 200)
- Compact frontend–backend API with a binary-based protocol
- Quick Look support on macOS
- File Explorer context-menu extension on Windows
- Improved
Datasetrendering
Zero-lag saving
We redesigned the entire system that controls how graphs, images, sound, and dataset objects flow among the evaluation kernel, frontend, and server. You no longer need to wait for the so-called synchronization of objects; the data is already there!

In addition, we improved our garbage collector for plots, graphics, and other heavy objects. It works in several steps:
- Silently marks anything not visible in the notebook as garbage
- Scans through cells and finds all false positives (objects that are not visible but are still in use)
- Scans through all hidden, encoded objects and marks other false positives
- Nukes the rest
Faster I/O
A multi-step refactor throughout the WLJS Notebook codebase gave us the opportunity to tighten and factor out the frontend–backend API. We no longer evaluate arbitrary expressions on the backend; instead, the backend accepts only a limited set of opcodes and non-executable payloads. The JavaScript → WL pipeline now relies on a binary protocol, an efficient WXF serializer implementation, and a zero-copy approach to typed arrays and binary payloads. Altogether:
- 2×
%3Cabbr%20title%3D%22it%20was%20always%20fast%2C%20actually%22%3Efaster%3C%2Fabbr%3E Kernel (WL) → Frontend (JavaScript) - 7,400× faster Frontend (JavaScript) → Kernel (WL)
You can literally stream raw, uncompressed video to the backend in real time using something like this:
img = ConstantArray[{255,255,255,255}, {240,320}];
EventHandler["takeMyData", (img = #)&];
Image[img // Offload, "Byte"] (*VB[*)(FrontEndRef["F989ee7a3-fcdc-4ced-9fbe-0af0e7c94fd7"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgUJW1pYpqaaJxrrpiWnJOuaJKem6FqmJaXqGiSmGaSaJ1uapKWYAwC0MxdT"*)(*]VB*) On the JavaScript side, we can capture frames from a webcam, obtain them from the GPU (WebGL/WebGPU), or generate them using Canvas:
.js
function createCanvasPixels(width = 320, height = 240) {
const canvas = new OffscreenCanvas(width, height);
const context = canvas.getContext("2d");
// Draw something
context.fillStyle = "#18181b";
context.fillRect(0, 0, width, height);
context.fillStyle = "#38bdf8";
context.beginPath();
context.arc(width / 2, height / 2, 60, 0, Math.PI * 2);
context.fill();
const { data } = context.getImageData(0, 0, width, height);
return new NumericArrayObject(data, [height, width, 4]); // Uint8ClampedArray
}
const pixels = createCanvasPixels();
server.kernel.io.fire('takeMyData', pixels); All typed arrays and NumericArrayObject instances (our class for structured typed arrays) become NumericArray objects on the Wolfram side, while everything else is translated as usual: [1,2,3...] becomes {1,2,3...}, objects become associations, and so on.
Faster initial launch and package loading
It turned out that our autocomplete feature, built around the $NewSymbol expression—which is called on every symbol allocation—was causing noticeable kernel lag, as well as silent context switching (the trickiest bug we have ever faced). We no longer use it; instead, we periodically scan contexts using the safer approach, Names[]. This sped up the initial launch by a factor of eight.
Better expression formatting
We improved our main editor so that it automatically adjusts brackets in cases like these:
{Red, 1} {(*VB[*)(RGBColor[1, 0, 0])(*,*)(*"1:eJxTTMoPSmNiYGAo5gUSYZmp5S6pyflFiSX5RcEsQBHn4PCQNGaQPAeQCHJ3cs7PyS8qYgCDD/ZQBgMDnAEA4iUPRg=="*)(*]VB*),1} ByteArray[{1,2,4}] ByteArray[(*VB[*) "AQIE" (*,*)(*"1:eJxdUFtPwjAUxmvUP2Dikz70HYpLmG8tMEARl50N2Z6cW1kWtnV0EC6/0x/kKWBEk+brdzk5PacPn9KZntVqteoOgcu1JVX+wZQKi0TEsMzzUG3Qnp7omnOEvsziHRnJQvzaw7Ra/FOnWt0etT20GyxEPk7FCi4xg3Qrnu73tTcI3ThdSKXjv0/u1TWC5ww7IpKxgC9UhLYIZZwTg+NNqLkzaJPQOhr0x8CkfZQjoQ3SZOJ57bqv0oZ8lPo9Jo0kAuZNjCCYlXMoCOUc3gO8JoUV+quk7T4m3fEa8sCeY0U0MRx/lpnelg28qFI2+H3J7V4bihEkZsFeZLVpmUPup5ZVhjNWNuo+JKTZ0UdPdzSR0eGHFfa/oTd3lpmAK01EGL8V2WbnumopqgskVphV4hsBZHON"*)(*]VB*)] BarLegend accepts row layouts
We extended the options for BarLegend to support horizontal bar legends as well:
BarLegend[{"Rainbow", {0,1}}, 2, LegendLayout->"Row", ImageSize -> {300,20}, LegendLabel->"Here"] (*VB[*)(FrontEndRef["F5afae585-657f-41bb-bf7e-f7e307e0dcc5"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgUJmyamJaaaWpjqmpmap+maGCYl6SalmafqArGxgXmqQUpysikAp3YWsw=="*)(*]VB*) It also supports an absolute offset:
BarLegend[{"Rainbow", {0,1}}, 2, LegendLayout->"Row", ImageSize -> {300,20}, LegendLabel->{"Here", {0,-35}}] (*VB[*)(FrontEndRef["Fc105fbc5-4612-4eac-8dc0-67ccfaf69b9a"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgUJJxsamKYlJZvqmpgZGumapCYm61qkJBvompknJ6clpplZJlkmAgCiFxan"*)(*]VB*) Here it is in action:
ContourPlot[ (*FB[*)(((*SpB[*)Power[x(*|*),(*|*)2](*]SpB*)-(*SpB[*)Power[y(*|*),(*|*)2](*]SpB*))(*,*)/(*,*)((*SpB[*)Power[((*SpB[*)Power[x(*|*),(*|*)2](*]SpB*)+(*SpB[*)Power[y(*|*),(*|*)2](*]SpB*))(*|*),(*|*)2](*]SpB*)))(*]FB*),
{x, -1, 1}, {y, -1, 1},
Contours -> 4,
PlotLegends -> Placed[BarLegend[Automatic, 2, LegendLayout->"Row", ImageSize -> {230,20}, LegendLabel->{"Bar", {0,-35}}], Bottom],
ImageSize -> {300, 300},
ColorFunction -> "Rainbow",
ClippingStyle -> Automatic
] %28%2AVB%5B%2A%29%28Legended%5BToExpression%5BFrontEndRef%5B%22F483c1046-3eef-475f-9d6d-023715ac2193%22%5D%2C%20InputForm%5D%2C%20Placed%5BBarLegend%5B%7BBlend%5B%22Rainbow%22%2C%20%231%5D%20%26%20%2C%20%7B-59%2F10%2C%2059%2F10%7D%7D%2C%202%2C%20LabelStyle%20-%3E%20%7B%7D%2C%20LegendLabel%20-%3E%20%7B%22Bar%22%2C%20%7B0%2C%20-35%7D%7D%2C%20LegendLayout%20-%3E%20%22Row%22%2C%20LegendMarkerSize%20-%3E%20300%2C%20ScalingFunctions%20-%3E%20%7BIdentity%2C%20Identity%7D%2C%20ImageSize%20-%3E%20%7B230%2C%2020%7D%2C%20Charting%60AxisLabel%20-%3E%20None%2C%20Charting%60TickSide%20-%3E%20Right%2C%20ColorFunctionScaling%20-%3E%20True%5D%2C%20Bottom%2C%20Identity%5D%5D%29%28%2A%2C%2A%29%28%2A%221%3AeJxlUslOAzEMLWXfJED8AAduVOrGUnGipUhIZdGk4kyaeIpFSKRMRrT8H5%2FBld8ocYYOzPRixfbzs5%2Fjo5GJ4uVKpZLsevOE8H4NwljujGXrPjKAMWjZzCBb3vQl%2BhwB4yWKHXhzY412fS37ExCp4yMF7JjC7YuWaNTbZ7UWQFxrn5%2FGtY48k7V6s3XeOOWi2ei04iqxrHgTpb5ugx7A5YNW0xAd2hRKGBqDgQIRWiWr1IqrBLIh17x5VFyAjLfJ3%2FSmy20m5I9pgInLFFDLm1QLh0ZneWLsKg8PK4g46pH5lUulTBmH5JXYqnO2iBMXV%2Fg5m81wK0cWcpfeCTmsFqhIYtj0gI9AMTdVEFcW%2B%2BTQ7fyXQkFxJracqS9Gkfjwyw9X4tr5xzU1aVYeee1F2F4Ou%2BP2FSzDD8CTpQUVhGPCa9Xj%2BYKT4iRhJbcStEM3LTglKvrF2zc%2BBupVUvNNag4Xl0in2Xvh1vn%2Bz1cTTMKCQv7e6NJVJfv%2F0UMUrwxldlwRjl%2FKW6d2PaOMnQv71ZnfbLjDrnHOvBV0%2FQDhqdK2%22%2A%29%28%2A%5DVB%2A%29 Improved Mathematica notebook imports
We should be honest with you here. Originally, the conversion process was hard-coded for certain cell types and expressions, which made it very fragile. The main issue is that Mathematica has many undocumented typesetting expressions mixed with textual constructs. Some are straightforward and can be mapped 1:1 to WLJS, while others require TeX forms to remain readable. At this point, we used AI assistance for the first time in this project. Through multiple iterations, the process was refined to the point where 10 random notebooks found online and in our archive could be opened and rendered correctly.
Here are some examples:

Better Dialog API
You no longer need to provide a Notebook or Window reference to open a dialog window:
Button["Message", MessageDialog["Hey"]]
Button["Open multiple files",
SystemDialogInputAsync["OpenList", Null]~Then~(MessageDialog@*ToString)
] Safer Refresh and Animate
If something refreshes too quickly, accidentally evaluating a large expression can be disastrous. To prevent this, the refresh rate is now automatically throttled to 5 FPS or lower, depending on the expression's size.
For example, ContourPlot generates an expression that is difficult for our built-in JIT to optimize for real-time updates, so it falls back to a full reevaluation. Even if we set RefreshRate to a high value, the system prevents the refresh rate from becoming unsafe:
Animate[ContourPlot[(*FB[*)(((x - r))(*,*)/(*,*)((*SpB[*)Power[(x - r)(*|*),(*|*)2](*]SpB*) + (*SpB[*)Power[(y)(*|*),(*|*)2](*]SpB*)))(*]FB*), {x,-1,1}, {y,-1,1}], {r,-1,1,0.1}, (*BB[*)(RefreshRate->60)(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRAeB5AILqnMSXXKr0hjgskHleakFnMBGU6JydnpRfmleSlpzDDlQe5Ozvk5+UVFDGDwwR6dwcAAAAHdFiw="*)(*]BB*)] Improved notebook-to-HTML export
We removed all the clutter and workarounds from our generated HTML notebooks.

The files are now smaller, search-engine-friendly, and based on Web Components. However, we retained compatibility with the older format, so your previously exported HTML notebooks can be reimported into the wln format. We have no plans to remove the old parser.

A single, self-contained HTML file that works without an internet connection and can be reimported as a normal notebook
We unified our export workflow. Whenever you export a WLJS Notebook to MDX/React, vanilla HTML/JavaScript, or a standalone HTML file:
- All narrative cells (Markdown) are rendered as standard HTML; for MDX, they are left intact
- The contents of all code cells are preserved as-is and wrapped in
wljs-editortags
The second point is very important: if a browser does not recognize this tag, the content is shown as normal, unstyled code. Once the Web Components bootstrap loads, the code is automatically highlighted and displayed as a normal-looking WLJS cell with graphics, sound, equations, and more. Here is our bootstrap package:
You can add it to your footer as a single script. It is framework-agnostic:
.html
<script
type="module"
src="https://cdn.jsdelivr.net/gh/WLJSTeam/web-components@latest/src/common/app.js"
></script>
It can also be self-hosted. Navigate to
If you are a React/Next.js user, no problem: here is an example blog project that uses this bootstrap.
Here is what it looks like in practice when you export the notebook as
.html
<h2>Contour Plot</h2>
<p>Generates a contour plot of f as a function of {x,y}</p>
<wljs-editor display="codemirror" type="Input">
<xmp>ContourPlot[
Cos[x] + Cos[y], {x, 0, 4 Pi}, {y, 0, 4 Pi}
]</xmp>
</wljs-editor>
<wljs-editor display="codemirror" type="Output">
<xmp>(*VB[*)(FrontEndRef["..."])(*,*)(*"1:..."*)(*]VB*)</xmp>
</wljs-editor> These wljs-editor tags are unstyled by default, but they still show meaningful content before the bootstrap has loaded. Even if JavaScript is disabled, the source code remains visible to users and search engines.
All visible objects, including Manipulate expressions, can be exported while preserving their interactivity.
Crash reports and server logs
If the master kernel crashes for any reason, logs will be generated. You can always access them from

Fixed race conditions in the MCP server
If you have ever used WLJS with coding agents, you might have noticed that they sometimes had to redo cell edits because of apparent typos. In fact, they were not typos! They were caused by lag in cell-state synchronization, which is no longer an issue in 3.2.0.
Fixed race conditions for inset expressions and labels
Duplicated labels or expressions inside Inset could cause race conditions and result in one or more missing visual elements. We eliminated this problem by aggregating all requests into a single task for each expression.
Batch cell operations in the command palette
We added a small snippet that lets you delete or add cells, starting from the focused cell.

JavaScript API changes
The old, unsafe methods were completely removed:
server.kernel.askserver.kernel.sendserver.kernel.emittserver.kernel._emitt
They were replaced by the new ServerIO class, introduced in 2025:
server.kernel.io.fire(eventId, data)– fires an event with data as its payloadserver.kernel.io.fire(eventId, data, pattern)– fires an event that matches the pattern (a string)server.kernel.io.poke(eventId)– fires an event with no payload (the fastest route)server.kernel.io.fetch(symbolName, args)– evaluatesApply[symbol, args]and returns the result (which may also be a Promise)server.kernel.io.fetch(symbolName)– evaluates a symbol and returns the result (which may also be a Promise)server.kernel.io.request(eventId, data)– works like thefiremethod but returns the result generated by the event (which may also be a Promise)server.kernel.io.request(eventId, data, pattern)– works likefirewith a pattern provided
NetGraph shows the preview of nets
Finally the output form of NetGraph can show something meaningful!
net = NetGraph[{LinearLayer[2], ElementwiseLayer[Ramp],
ThreadingLayer[Plus]}, {1 -> 2, {NetPort["Input"], 2} -> 3}] NetGraph%5B%28%2AVB%5B%2A%29%20%3C%7C%22Type%22%20-%3E%20%22Graph%22%2C%20%22Inputs%22%20-%3E%20%3C%7C%22Input%22%20-%3E%20NeuralNetworks%60TensorT%5B%7B2%7D%2C%20NeuralNetworks%60AtomT%5D%7C%3E%2C%20%22Outputs%22%20-%3E%20%3C%7C%22Output%22%20-%3E%20NeuralNetworks%60TensorT%5B%7B2%7D%2C%20NeuralNetworks%60AtomT%5D%7C%3E%2C%20%22Nodes%22%20-%3E%20%3C%7C%221%22%20-%3E%20%3C%7C%22Type%22%20-%3E%20%22Linear%22%2C%20%22Arrays%22%20-%3E%20%3C%7C%22Weights%22%20-%3E%20NeuralNetworks%60TensorT%5B%7B2%2C%202%7D%2C%20NeuralNetworks%60RealT%5D%2C%20%22Biases%22%20-%3E%20NeuralNetworks%60Nullable%5BNeuralNetworks%60TensorT%5B%7B2%7D%2C%20NeuralNetworks%60RealT%5D%5D%7C%3E%2C%20%22Parameters%22%20-%3E%20%3C%7C%22OutputDimensions%22%20-%3E%20%7B2%7D%2C%20%22%24OutputSize%22%20-%3E%202%2C%20%22%24InputSize%22%20-%3E%202%2C%20%22%24InputDimensions%22%20-%3E%20%7B2%7D%7C%3E%2C%20%22Inputs%22%20-%3E%20%3C%7C%22Input%22%20-%3E%20NeuralNetworks%60TensorT%5B%7B2%7D%2C%20NeuralNetworks%60AtomT%5D%7C%3E%2C%20%22Outputs%22%20-%3E%20%3C%7C%22Output%22%20-%3E%20NeuralNetworks%60TensorT%5B%7B2%7D%2C%20NeuralNetworks%60RealT%5D%7C%3E%7C%3E%2C%20%222%22%20-%3E%20%3C%7C%22Type%22%20-%3E%20%22Elementwise%22%2C%20%22Arrays%22%20-%3E%20%3C%7C%7C%3E%2C%20%22Parameters%22%20-%3E%20%3C%7C%22Function%22%20-%3E%20NeuralNetworks%60ValidatedParameter%5BRamp%5D%2C%20%22%24Dimensions%22%20-%3E%20%7B2%7D%7C%3E%2C%20%22Inputs%22%20-%3E%20%3C%7C%22Input%22%20-%3E%20NeuralNetworks%60TensorT%5B%7B2%7D%2C%20NeuralNetworks%60RealT%5D%7C%3E%2C%20%22Outputs%22%20-%3E%20%3C%7C%22Output%22%20-%3E%20NeuralNetworks%60TensorT%5B%7B2%7D%2C%20NeuralNetworks%60RealT%5D%7C%3E%7C%3E%2C%20%223%22%20-%3E%20%3C%7C%22Type%22%20-%3E%20%22Threading%22%2C%20%22Arrays%22%20-%3E%20%3C%7C%7C%3E%2C%20%22Parameters%22%20-%3E%20%3C%7C%22Function%22%20-%3E%20NeuralNetworks%60ValidatedParameter%5BPlus%5D%2C%20%22Broadcasting%22%20-%3E%20NeuralNetworks%60ValidatedParameter%5BAutomatic%5D%7C%3E%2C%20%22Inputs%22%20-%3E%20%3C%7C%22Input1%22%20-%3E%20NeuralNetworks%60TensorT%5B%7B2%7D%2C%20NeuralNetworks%60AtomT%5D%2C%20%22Input2%22%20-%3E%20NeuralNetworks%60TensorT%5B%7B2%7D%2C%20NeuralNetworks%60RealT%5D%7C%3E%2C%20%22Outputs%22%20-%3E%20%3C%7C%22Output%22%20-%3E%20NeuralNetworks%60TensorT%5B%7B2%7D%2C%20NeuralNetworks%60AtomT%5D%7C%3E%7C%3E%7C%3E%2C%20%22Edges%22%20-%3E%20%7BNeuralNetworks%60NetPath%5B%22Nodes%22%2C%20%223%22%2C%20%22Inputs%22%2C%20%22Input1%22%5D%20-%3E%20NeuralNetworks%60NetPath%5B%22Inputs%22%2C%20%22Input%22%5D%2C%20NeuralNetworks%60NetPath%5B%22Nodes%22%2C%20%221%22%2C%20%22Inputs%22%2C%20%22Input%22%5D%20-%3E%20NeuralNetworks%60NetPath%5B%22Inputs%22%2C%20%22Input%22%5D%2C%20NeuralNetworks%60NetPath%5B%22Nodes%22%2C%20%222%22%2C%20%22Inputs%22%2C%20%22Input%22%5D%20-%3E%20NeuralNetworks%60NetPath%5B%22Nodes%22%2C%20%221%22%2C%20%22Outputs%22%2C%20%22Output%22%5D%2C%20NeuralNetworks%60NetPath%5B%22Nodes%22%2C%20%223%22%2C%20%22Inputs%22%2C%20%22Input2%22%5D%20-%3E%20NeuralNetworks%60NetPath%5B%22Nodes%22%2C%20%222%22%2C%20%22Outputs%22%2C%20%22Output%22%5D%2C%20NeuralNetworks%60NetPath%5B%22Outputs%22%2C%20%22Output%22%5D%20-%3E%20NeuralNetworks%60NetPath%5B%22Nodes%22%2C%20%223%22%2C%20%22Outputs%22%2C%20%22Output%22%5D%7D%7C%3E%2C%20%3C%7C%22Version%22%20-%3E%20%2215.0.2%22%2C%20%22Unstable%22%20-%3E%20False%7C%3E%20%28%2A%2C%2A%29%28%2A%221%3AeJxTTMoPSmNmYGAolgISTvkVbvlFuQmORUWJeempKcGlubmJRZVA4TRGkBohIOFWlJ9X4pqX4lqRmlxakpiUkxqsChJOMjI3NzRPMtVNNTIw0DUxNjLSTbIwNtQ1Nk00NUq2TDW0TDNLYwCZwgIkfDKLS4pZQfoSc4pTAbJZIuc%3D%22%2A%29%28%2A%5DVB%2A%29%5D More integration tests
We took testing even more seriously and added additional integration tests (over 200). Each test uses a fully functional notebook environment running in a headless Chromium browser. Our tests cover:
- The full cell-evaluation cycle
- UI interactions
- Notebook imports
- Simple and complex 2D and 3D graphics, as well as sound
DatasetandTabulardata structures with mixed row types- Solving equations and rendering visual output
- All GUI building blocks, from
InputRangetoManipulate - Expression styling
- WLX, HTML, and JavaScript cells
- Neural networks
Native Windows and macOS extensions
We added automatic File Explorer context-menu integration with a New WLJS Notebook entry. In addition, Quick Look is now available on macOS: press


Thank you for your support
This time, we have two new monthly supporters on OpenCollective ❤️
- Silvas
- Anonymous Guest