Lustre v5.5.0 released!
Things have been quiet over here for the last few months, but we’re launching into 2026 with a release that makes Lustre even faster and plugs some gaps that improve its production-readiness.
Optimising render performance with element.memo
Like many other frameworks, Lustre works by implementing a virtual DOM. This
means when your view function is called, instead of constructing HTML elements
directly we instead produce a lightweight representation of the DOM and calculate
the minimal set of DOM updates necessary to bring things up to date.
Lustre’s vdom implementation is highly optimised and should not be a problem for
many production workloads. For performance-sensitive applications, however, other
frameworks often provide ways to hint to the runtime when rendering can be skipped.
In React this is hooks like useCallback or useMemo. In Elm, these are functions
in Html.Lazy.
Lustre now also gains this ability, with the element.memo and element.ref
functions. When used effectively, these functions can massively cut down Lustre’s
render time: adding a single element.memo call to our TodoMVC benchmark app
improved performance by 60%!

And let’s take a look at the code added to the benchmark app:
fn view_entry(entry: Entry) {
+ use <- element.memo([element.ref(entry)])
let Entry(description:, id:, completed:, editing:, ..) = entry
html.li(
[attribute.classes([#("completed", completed), #("editing", editing)])],
[...],
)
}
element.memo requires two things to work: a list of dependencies - constructed
with element.ref - and a view function that takes no arguments and returns a
normal Lustre Element.
The first time Lustre renders a memoised element, it immediately evaluates the view function and caches the list of dependencies. During subsequent renders, Lustre will first check each dependency in the list with the new dependency list for reference equality. This is a special kind of equality in JavaScript that doesn’t just check if two values share the same structure but also if they share the same location in memory, making it a very quick check to make.
If all dependencies are reference equal, Lustre does nothing else: it does not need to call the view function and diff the vdom and instead it can move on to the rest of the diff.
In cases like TodoMVC where we render many of the same element in a list but only
one changes at a time, element.memo means Lustre spends almost no time at all
on parts of the vdom that haven’t changed.
You might be tempted to slap element.memo on everything and reap infinite
performance benefits, but it’s not quite that simple. Specifically, there are
three main points to take note of when considering element.memo:
-
Values in the dependency list are checked for reference equality only. This is an important departure from Gleam’s semantics which only has structural equality defined. That means if you construct something like a new custom type ever render, even if the values contained don’t changed the reference to the underlying JavaScript object does change, invalidating the memo cache.
-
For dependencies that regularly change,
element.memobecomes pure overhead; incurring a memory cost of storing those dependencies and runtime cost to regularly invalidating the cache and re-evaluating the view. -
It is very easy to accidentally leave out a dependency from the list and this can be tremendously difficult to debug!
With that in mind, most apps can happily continue to work without ever needing
to reach for element.memo. But now the few apps that need this extra performance
have the tools they need to squeeze it out in a pinch. A huge thanks to
rebecca~ for getting this done, it took
us many attempts!
Better virtualisation
While we’re thinking about the vdom, let’s talk about virtualisation. When you
mount a client Lustre application onto a DOM node that already has some content
inside of it, Lustre will convert the existing DOM into a vdom tree that the first
view result can diff against.
This is a technique used to make sure Lustre doesn’t waste time reconstructing elements that already exist and works great when an app is prerendered on the server, but our implementation wasn’t perfect.
In particular, using element.fragment or element.map in the prerendered HTML
would end up virtualising to a slightly different vdom as those elements don’t
exist in the HTML at all! This new release now produces comments to mark where
fragment, map, and memo nodes start and end meaning virtualisation is much
more accurate.
Once again we have rebecca~ to thank for
landing these improvements as part of her work on element.memo!
Tighter integration with gleam/otp
Lustre is a universal framework, capable of not just rendering HTML strings on the server but also running full applications that stream UI updates over a WebSocket. These are called server components and when running on the Erlang target they spawn a new process when started.
One of the BEAM’s biggest strengths (that’s the virtual machine Erlang and Gleam run on) is it’s fault tolerance achieved through a tree of supervisors: special processes that can isolate crashes and restart failed parts of an application without affecting other parts.
Until now a server component started on the Erlang target has been unable to participate in supervision due to Lustre’s constraints as a framework capable of running on all of Gleam’s targets. Lustre v5.5.0 remedies this by introducing three new functions specifically for participating in OTP and supervision:
-
lustre.supervisedfollows established Gleam conventions and produces aChildSpecificationthat can be used with Gleam’s static supervisors. -
lustre.namedcan be used to assign a global unique name to the runtime. This name can be used to recover aSubjectfor the server component runtime regardless of whether it has been restarted by a supervisor or not. -
lustre.factorycan be used to create agleam/otpfactory supervisor capable of
These three additions make Lustre’s server components much more suitable for use in production. We look forward to seeing what folks build!
And the rest
Users rendering SVG elements to string in Lustre should see significantly less-noisy
output as Lustre is now much smarter about when to include xmlns attributes. A
handful of missing attributes have been added – if you spot any more, PRs are
always welcome! And finally, internally Lustre has begun migrating it’s JavaScript
FFI code to use Gleam’s newer data representation.