Skip to content

Batch: Support browser extensions, Google Translate, fix Html.map, and more #187

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

Open
wants to merge 110 commits into
base: master
Choose a base branch
from

Conversation

lydell
Copy link

@lydell lydell commented Jun 1, 2025

Intro

This PR:

  • Fixes basically all bugs related to the (virtual) DOM.
  • Makes Elm work with browser extensions and Google Translate – and might be the first framework to do so. End users for the win!
  • Does not change performance in any way that I’ve been able to measure.
  • Is 99.99 % backwards compatible.
  • Is used in production at Insurello since 2025-05-26.

For those who would like to use this PR in their own app – see https://github.com/lydell/elm-safe-virtual-dom.

The above link also explains exactly what is changed, in a way that should be somewhat understandable even if you’re not super into the real and virtual DOM.

Below is a more technical, condensed version of the above link.

Summary of changes and fixed issues

The key changes

Making Elm work well with browser extensions, third-party scripts and page translators required three key changes.

1. Attaching DOM nodes on the virtual nodes

A render in Elm currently works like this:

  1. Run view.
  2. Diff with the previous virtual DOM, producing patches.
  3. Attach a DOM node to each patch (_VirtualDom_addDomNodes).
  4. Apply the patches.

It’s step 3 that can crash if a browser extension has changed the DOM.

With this PR, there is no more step 3. Instead, we store the DOM node on the virtual node. This means that we don’t even need patches. While diffing in step 2, we simply make the changes immediately to the correct DOM node. Since there’s no walking of the DOM tree, it doesn’t matter what changes a browser extension makes.

(However, since the same virtual DOM node can appear more than once in the tree, it’s a bit more complicated than that – see New algorithm for pairing virtual DOM nodes.)

2. Cooperating with page translators

There’s some new code for cooperating with page translators (Google Translate, as well as the translators built into Firefox and Safari). If the diff tells us to update a text node, but we detect that the text node in question has been translated, we tell the parent element of that text node to delete all text node children and re-render them. The page translator then kicks in and re-translates that element (taking the entire text of the element into account for a more accurate translation).

(Evan, if you remember us talking about this at Elm Camp 2024 – don’t worry, there is no “bug” introduced on purpose regarding <font> elements – they can still be created by Elm just fine. Google Translate oddly uses <font> tags for translated text, but it turned out to easy to tell the difference between <font> tags created by Elm and by others.)

3. Virtualizing more conservatively

When using Browser.document and Browser.application, Elm takes charge of <body>. Third-party scripts and browser extensions put things in <body> too, and crucially they might do so before Elm initializes. Currently, Elm virtualizes all elements in the mount node (<body>) in this case, which most likely results in the extra things in <body> added by third-party scripts and browser extensions being removed.

This PR changes the virtualization behavior to only virtualize text nodes, and elements that have the data-elm attribute, leaving everything else alone. I add data-elm automatically to all elements created by Elm, so if you server-side render your page by running your Elm code on the server, you get that for free without having to do anything. You can read more about how this can be used in practice at elm-pages PR 519.

Note: This is the reason the PR is only 99.99 % backwards compatible. Read more in the “Breaking” changes section below.

The other changes

So, I’ve talked about the key changes. But the “Summary of changes and fixed issues” section mentions a few more things. Why are they in this PR?

  • Fixed Html.map: I needed to work on Html.map anyway to make it work with the new DOM node pairing algorithm.
  • Improved Html.Keyed: Same thing.
  • Virtualization: I needed to touch virtualization to support third-party scripts better, so I ended up going all the way with it.
  • CSS custom properties and namespaced attributes: I needed to work on the diffing code anyway, since it now applies changes directly to DOM nodes instead of creating patches, and it was way easier to do all the changes in one batch and test it thoroughly once in production, rather than juggling with a stack of PR:s.
  • lazy fix for inputs: It was incredibly easy to fix thanks to the other changes in this PR.

Detailed descriptions of changes

“Breaking” changes

I haven’t changed the Elm interface at all (no added functions, no changed functions, no removed functions, or types). All behavior except one detail should be equivalent, except less buggy.

The goal was to be 100 % backwards compatible. For some people, it is. For others, there’s one change that is in “breaking change territory” which can be summarized as: Elm no longer empties the mount element. It’s easily fixed by adding the data-elm attribute to select elements in the HTML.

That’s how users will perceive it. In reality, the actual change is which elements are virtualized (to support third-party scripts better, as mentioned in the “Virtualizing more conservatively” section).

Read all about these “Breaking” changes in the safe-virtual-dom documentation.

Performance

  • The well-known js-framework-benchmark includes Elm. I ran that Elm benchmark on my computer, with and without my PR:s, and got the same numbers (no significant difference).
  • When testing with some large Elm applications at work, I couldn’t tell any performance difference with the PR:s. (Neither by eye nor by profiling.)
  • Both the official elm/virtual-dom and my PR have O(n) complexity.
  • The official elm/virtual-dom algorithm sometimes does more work, but other times this PR does more work. It seems to even out.

Related PR:s

Code style

I’ve done my best to:

  • Follow the existing code style.
  • Not introduce any unnecessary changes.
  • Use the same object key order when constructing objects (for performance).

lydell added 23 commits February 1, 2025 13:26
`__oldDomNodes` could have gotten the name `i` when compiled and broken things.
It only worked before because of luck.
According to this benchmark, setting .data is faster than .replaceData():

https://jsbench.me/80m6mbx5l1/1
Instead of at `Number.MIN_SAFE_INTEGER`. While `Number.MIN_SAFE_INTEGER`
doubles the amount of representable integers, there are reasons not to
use it:

- Debugging numbers near zero is easier, than numbers with a lot of
  digits.
- In elm/browser I added another ever-increasing counter for animation
  frames, and there it was not possible to use negative numbers. So we’d
  run out of animation frame counting before we run out of render
  counting anyway.
- It would still take like 25 000 years until we overflow.
- It wouldn’t surprise me if JS engines can optimize numbers that are
  near zero in some way.
lydell added 3 commits June 7, 2025 00:10
One might think that if `oldNode === newNode` no changes are needed,
but users can mutate properties, for example by typing into text inputs,
so we still need to apply properties.

This happens when using constants or `lazy`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment