Blog

Chrome's built-in translator can crash your React app

Diario del capitán, fecha estelar d212.y42/AB

Marta Armada
Tech Lead
Jigsaw

For weeks, one of our projects had a production error that nobody could reproduce:

NotFoundError: Failed to execute 'insertBefore' on 'Node'

It happened often, but the reports had no useful reproduction steps and no component stack pointing to an obvious culprit.

The clue came from a support ticket. A user sent us a screen recording of the app crashing after a completely ordinary state change. The interaction looked normal. The only unusual detail was that Chrome was translating the page.

Once we tried the same flow with translation enabled, the crash finally made sense.

What the translator changes

Chrome's built-in translator can replace translatable text nodes with nested <font> elements.

Something that starts like this:

<div>Hello</div>

may end up looking roughly like this:

<div>
  <font>
    <font>Hallo</font>
  </font>
</div>

The original text node is no longer attached to the document, but React still holds a reference to it from the previous render.

This does not always crash immediately. If React updates the detached text node, it writes to something the user can no longer see: the visible translation stays unchanged, which can make counters and labels look frozen. Removing the parent element usually works too, because React is removing a real element that still exists. The trouble starts when React tries to operate on the detached text node itself, typically through removeChild or insertBefore.

A button, a spinner and a detached text node

Consider a button that displays a spinner while an action is running:

<button disabled={loading}>
  {loading && <Spinner />}
  {loading ? "Saving..." : "Save"}
</button>

Before the action starts, the button contains a single text node:

<button>Save</button>

Chrome translates the text and replaces that node. React still remembers the original node as a child of the button.

When loading becomes true, React needs to insert the spinner before the text:

button.insertBefore(spinner, originalTextNode);

But originalTextNode is no longer a child of the button. The browser throws a NotFoundError, and the React render can bring down the application.

Possible fixes

Wrap the text in an element

For this button, the most straightforward fix is to give React a stable element to reference:

<button disabled={loading}>
  {loading && <Spinner />}
  <span>{loading ? "Saving..." : "Save"}</span>
</button>

Chrome may still replace the text inside the <span>, but the <span> itself remains a child of the button. When the loading state begins, React can insert the spinner before that element:

button.insertBefore(spinner, span);

This is actually how we introduced the bug in the first place: we removed some <span> wrappers that looked unnecessary, without realising they were the only stable boundary between React and the translator's DOM changes.

That does not mean every string in a React application needs a <span>. Adding wrappers everywhere has its own cost: with display: flex and gap, extra elements can produce inconsistent spacing, and the markup becomes noisier for no real gain. Wrapping is worth it when bare text has siblings that can be conditionally inserted, removed or reordered.

Remount the whole block

Another option is to change the key when the state changes:

<div key={loading ? "loading" : "idle"}>
  {loading && <Spinner />}
  {loading ? "Saving..." : "Ready"}
</div>

React replaces the whole element instead of reconciling children inside the translated subtree.

This worked well for the small display component where we first hit the problem. It is less suitable for interactive elements like buttons, inputs or larger components, because remounting resets focus, local state and uncontrolled values.

Disable translation for the affected subtree

The HTML translate attribute can prevent translation inside an element:

<button disabled={loading} translate="no">
  {loading && <Spinner />}
  {loading ? "Saving..." : "Save"}
</button>

This prevents the DOM mutation, but it also prevents the user from translating the button label. That makes it a poor default for ordinary UI copy.

That said, it is the right fix for content that should never be translated in the first place, such as font ligature icons:

<span className="material-icons" translate="no">
  delete
</span>

These icon libraries use text like delete to select a glyph. If the translator changes the word, the glyph stops resolving and the icon can turn into visible text. The translate attribute is inherited, so it can usually go on a shared icon container instead of every individual icon.

Why it was difficult to trace

The crash only appeared after a specific sequence:

  1. The user translated the page.
  2. The translator replaced the relevant text node.
  3. A later state change inserted an element before that node.
  4. React tried to use its stale DOM reference.

Error monitoring captured the failed insertBefore, but not the browser translation that had modified the DOM earlier. Without seeing the translated page, the exception looked like an impossible React state.

This behavior has been tracked since 2017 in facebook/react#11538. The issue is closed, but there is no general React-side fix for DOM mutations performed by browser translators and extensions.

The bug looks strange at first, but the pattern behind it is more common than it seems: any bare text node sitting next to a conditionally rendered sibling is a candidate. The fixes are all fairly simple, and none of them is universally the right one. Wrapping the text works for buttons and inline copy, remounting works for small display components, and translate="no" is the right call only for content that should never be translated at all. Pick your fix case by case instead of applying one rule everywhere.

Compartir este post

Artículos relacionados

React

React vulnerability (CVE-2025-66478): How we responded and what you should do

A critical React SSR vulnerability (CVE-2025-66478, CVSS 10.0) allows remote code execution. We have swiftly patched all active React/Next.js client projects. Users must apply the official fix immediately.

Leer el artículo
Bird migration

Replacing jQuery with React: a pragmatic migration plan (with real estimates)

Legacy frontends rarely fail loudly. They fail quietly: slower iteration, harder UI changes, more edge-case bugs, and a growing sense that every improvement costs too much. That is usually the moment teams consider moving from a jQuery-heavy UI to a component-based frontend like React.

Leer el artículo
Updates to shadcn/ui

Updates to shadcn/ui: Base UI support and new Theme Editor

The latest shadcn/ui updates: v0 theme integration, Base UI support, and what it means for the React ecosystem.

Leer el artículo