Команда JavaScript for Devs подготовила перевод статьи о том, почему Google Переводчик может ломать React и другие современные веб-приложения. Причина в том, что расширение вмешивается в DOM, нарушая работу виртуального DOM и вызывая ошибки вроде removeChild и insertBefore. Автор показывает реальные кейсы, обходные пути и поднимает важный вопрос: имеет ли фреймворк право на полный контроль над DOM?
Google Переводчик, встроенное расширение Google Chrome, — это машинный переводчик, который дает пользователям простой способ переводить веб-страницы прямо во вкладке браузера. Это позволяет людям со всего мира пользоваться веб-страницами независимо от родного языка.
Но у этого преимущества есть обратная сторона: расширение вмешивается в работу многих современных сайтов. Причина в том, что Google Переводчик изменяет DOM так, что ломает сами приложения. Чаще всего это проявляется падениями из-за нативного метода DOM-элемента removeChild, приводя к ошибкам вроде NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node., но на самом деле проблем больше. И не все из них так очевидны, как явный креш.
Но у этого преимущества есть обратная сторона: расширение вмешивается в работу многих современных сайтов. Причина в том, что Google Переводчик изменяет DOM так, что ломает сами приложения. Чаще всего это проявляется падениями из-за нативного метода DOM-элемента removeChild, приводя к ошибкам вроде NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node., но на самом деле проблем больше. И не все из них так очевидны, как явный креш.
Основной акцент этой статьи — на том, как Google Переводчик вмешивается в работу React, но важно понимать, что эти проблемы не уникальны для React; они характерны для большинства машинных переводчиков и способны нарушить работу любого большого и сложного веб-приложения.
В этой статье мы рассмотрим:
Как работает Google Переводчик
Как Google Переводчик вмешивается в работу приложений
Вмешательство браузерных расширений в целом
Влияние на обычный JavaScript-код
Возможные обходные пути и альтернативы
Кроме того, в конце статьи вы найдете приложение, где я делюсь мнением о том, стоит ли веб-приложениям вообще претендовать на полный и исключительный контроль над DOM.
Но сначала посмотрим, как работает Google Переводчик.
Как работает Google Переводчик
Чтобы понять, что делает Google Переводчик, нужно внимательно посмотреть на структуру DOM до и после перевода.
Весь HTML, который отрисовывается в браузере, представлен в JavaScript через DOM. Это древовидная структура, где каждый элемент — это узел. HTML-элементы представлены узлами типа Element, а текст — узлами типа TextNode.
Возьмем простой фрагмент HTML:
<p>There are 4 lights!</p>
В JavaScript это представлено в DOM примерно такой структурой:

Когда включается Google Переводчик, он ищет узлы TextNode для перевода. Эти узлы затем заменяются элементами FontElement с новыми, переведёнными строками внутри. В итоге HTML становится таким (предположим, переводим на нидерландский):
<p><font>Er zijn 4 lampen!</font></p>
И что ещё важнее, структура DOM становится такой:


Видно, что исходный TextNode демонтируется и заменяется новым FontElement с переведённым текстом внутри.
В общих чертах так Google Переводчик влияет на DOM — и это важная причина, почему он создаёт проблемы (то есть вмешивается) в работу JavaScript-приложений, которые сами манипулируют DOM.
Имитация работы Google Переводчика
Теперь, когда мы понимаем, как работает Google Переводчик, мы можем смоделировать его применение к части страницы. Это позволит нам проще воспроизводить проблемы, вызываемые Google Переводчиком.
Ниже приведен фрагмент, который ищет элемент с id «translateme» и заменяет всех его прямых дочерних узлов типа TextNode на FontElement — примерно так же, как делает Google Переводчик. Чтобы нагляднее показать, к какому тексту применена симуляция Google Переводчика, любой затронутый текст обрамляется квадратными скобками («There are 4 lights!» превращается в «[There are 4 lights!]»).
useEffect(() => {
document.getElementById('translateme').childNodes.forEach((child) => {
if (child.nodeType === Node.TEXT_NODE) {
const fontElem = document.createElement('font')
fontElem.textContent = `[${child.textContent}]`
child.parentElement.insertBefore(fontElem, child)
child.parentElement.removeChild(child)
}
})
})
Все примеры воспроизведения ниже используют этот метод для имитации работы Google Переводчика.
Ручное тестирование Google Переводчика
Если вы хотите сами проверить проблемы, вызываемые Google Переводчиком, это можно сделать вручную. Так вы лучше поймете, как Google Переводчик влияет на ваше приложение.
Самый простой способ, который я нашел, — переводить с английского на другой язык. Чтобы заставить Google Chrome делать это автоматически, нужно изменить раздел «Предпочитаемые языки» в настройках следующим образом:

Далее откройте веб-страницу, которую хотите протестировать. Если страница настроена корректно (и она на английском), в теге html должен стоять атрибут lang="en". Это позволяет Google Переводчику надежно определить язык и перевести страницу. Если он не предлагает перевод сам, нажмите значок перевода в адресной строке.

Проблемы из-за вмешательства
Теперь, когда мы понимаем, как Google Переводчик меняет DOM, можно разобрать проблемы, к которым это приводит. Самые распространенные из них:
Проблема: переводимый текст перестает обновляться
Когда Google Переводчик демонтирует узлы DOM и вставляет на их место собственные, исходные узлы продолжают существовать в памяти. Любые изменения, которые вы затем вносите в эти исходные узлы, никак не отобразятся в браузере пользователя. Они так и останутся в памяти.
Это создает проблему для систем вроде React, которые работают с виртуальным DOM. Одна из ключевых причин использовать виртуальный DOM — производительность, и важная часть подхода в том, чтобы по возможности обновлять значения узлов DOM, а не заменять их целиком. Замена узлов дороже вычислительно.
Следствие: в React любые текстовые значения или числа, которые могут меняться вместе с другой строкой, оказываются затронуты. После применения Google Переводчика значения на странице могут больше никогда не обновиться.
Для любого приложения, показывающего важные данные, это серьезная проблема — то есть фактически для любого крупного React-приложения. Неверные данные могут вводить в заблуждение и даже быть опасными. Дашборд с неправильным числом может привести к неверным решениям, показ некорректных цен может создать юридические риски, а неверная дозировка лекарства — куда более тяжелые последствия. Масштаб риска зависит от вашего приложения и бизнеса.
Эта проблема трудно обнаруживается, потому что не приводит ни к падению, ни к явной ошибке.
Воспроизведение
В примере ниже у нас простой счетчик, который отслеживает количество ламп (число хранится в useState). Кнопка увеличивает число ламп на единицу при каждом нажатии. Отмеченная подпись рядом с ней — это просто There are {lights} lights! — без условий и прочих усложнений.
Мы симулируем работу Google Переводчика описанным выше способом. Симуляция оборачивает затронутый текст в квадратные скобки, чтобы показать, что она активна. Значение, показанное зелёным под кнопкой, — это фактическое значение, на которое Google Переводчик не влияет.
Демо доступно по ссылке.

Когда вы нажмёте кнопку несколько раз, вы увидите, что состояние обновляется и компонент повторно рендерится, но переведённый текст так и не обновляется, чтобы отразить новое значение.

В примере воспроизведения вокруг текста видно три набора скобок. Это потому, что React создаёт отдельный
TextNodeдля каждой переменной внутри строки. Реальный Google Переводчик нормализует текстовые узлы, объединяя их, но наша эмуляция ради простоты этого не делает. Из-за этого воспроизведение немного отличается от поведения Google Переводчика, но результат остаётся тем же.
Проблема: креши
Если вы используете инструмент мониторинга ошибок вроде Sentry или пробовали вручную тестировать Google Переводчик, вы, скорее всего, уже видели подобные ошибки. В React из-за вмешательства Google Переводчика часто встречаются такие сообщения:
NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
Когда возникает одна из этих ошибок, React демонтирует ваше дерево до ближайшей границы ошибок (Error Boundary). Но если границы ошибок нет (а на сайтах такое встречается часто), упадет всё приложение.
Ошибка removeChild обычно появляется, когда ваше приложение пытается удалить из DOM условно отрендеренный TextNode, который Google Переводчик уже демонтировал. Ошибка insertBefore реже, но возникает в ситуациях, когда что-то, что рендерится по условию, пытается вставиться перед TextNode, который Google Переводчик уже демонтировал.
Во многих случаях такие креши могут быть менее критичны, чем ситуация с не обновляющимся переведённым текстом. Застывший текст менее предсказуем и может вводить пользователей в заблуждение, что хуже, чем не показать ничего вовсе.
Воспроизведение
Кнопка ниже переключает, включен ли свет, просто инвертируя булево значение в useState. Когда свет выключен, текст «There are 4 lights!» больше не будет отрисован по условному выражению {lightsOn && 'There are 4 lights!'}. React пытается привести рендер к согласованному состоянию, удаляя TextNode из того родителя, в которого он был добавлен. При активном Google Переводчике этот TextNode уже не является дочерним элементом родителя, что приводит к крешу.
Демо доступно по ссылке.

Чтобы воспроизвести проблему, у условного TextNode должен быть любой сосед (сиблинг). В React почти каждый узел, который рендерится по условию, имеет соседа, так что это типичная ситуация.
Другой способ воспроизвести этот креш — отрендерить разное количество TextNode внутри тернарного оператора. В примере ниже мы тоже переключаем свет, но вместо того чтобы ничего не показывать, когда свет выключен, пробуем вывести текст «The lights are off» через тернарный оператор: {lightsOn ? <>There are {lights} lights!</> : <>The lights are off</>}.
Демо доступно по ссылке.

Важно, что в этом воспроизведении у ветвей тернарного оператора разное количество TextNode. Хотя это не сразу заметно, здесь именно так: для выражения <>There are {lights} lights!</> React создает три TextNode.
Это упрощенная версия того, что может встречаться в вашем приложении. В примере кода мы могли бы использовать одну шаблонную строку для обеих ветвей тернарного оператора. В реальном мире такие выражения обычно сложнее, поэтому преобразовать их в одну шаблонную строку бывает трудно.
Поскольку способов варьировать количество рендерящихся TextNode много, уверен, что существует еще больше способов воспроизвести этот креш. Из-за этого сложно найти обходной путь, который закроет все случаи.
Обходные пути
Креши React описаны в этом ишью на GitHub. Было предложено несколько обходных путей, но, к сожалению, ни один из них не даёт быстрого решения. Некоторые даже усугубляют ситуацию.
Ниже перечисленные обходные пути касаются только крешей и никак не влияют на проблему с тем, что переведённый текст не обновляется.
1. Монкипатчинг removeChild и insertBefor
Gaearon, участник Core-команды React, предложил обходной путь, который монкипатчит removeChild и insertBefore, заставляя их «молчаливо» завершаться, если вызваны с некорректными аргументами.
Хотя такой монкипатч действительно предотвращает креши, он совсем не решает корневую проблему. Вместо того чтобы упасть, когда React пытается удалить TextNode через removeChild, ничего не происходит, и переведённый текст остаётся в DOM, пока не будет удалён его родитель. А при ошибке insertBefore заново отрендеренный текст просто не появится у пользователя.
Если пользователь не в курсе такого поведения, оба случая делают приложение почти столь же непригодным к использованию, как и при креше.
Посмотрите, как работает этот монкипатч:
Демо доступно по ссылке.

Вы можете включать и выключать эмуляцию Google Переводчика, чтобы увидеть, как компонент ведёт себя с его вмешательством и без него. Это также удобный способ «сбросить» компонент в исходное состояние.
2. Оборачивать TextNode в span
Пользователь GitHub Shuhei предложил обходной путь: оборачивать весь условно рендерящийся и соседний текст в элементы span. Это избегает крешей, потому что React перестаёт напрямую удалять или вставлять TextNode.
Это действительно исправляет некоторые из самых частых крешей, но не все. Креши, вызванные условно рендерящимися TextNode, как в выражении {lightsOn && 'There are 4 lights!'} из первого примера выше, таким способом устраняются. Но креши из-за других условно рендерящихся TextNode, например в воспроизведении с тернарным оператором, — нет.
Реализация такого обходного пути требует основательно переписать массу обычного кода. Без правила ESLint, которое будет это навязывать, вам придётся долго убеждать в PR-ах всю команду последовательно применять такой подход. И для многих честный вывод будет в том, что усилия и ухудшение качества кода того не стоят.
Плагин ESLint eslint-plugin-sayari содержит правило, требующее, чтобы
TextNode, которые делят общего родителя с другими элементами, были обёрнуты в тег<span>. Хотя это, вероятно, ловит проблемные выражения, у правила чрезвычайно высокий уровень ложных срабатываний — оно заставит вас оборачивать почти всеTextNodeв приложении. Креши из-за тернарного оператора этим правилом также не решаются.
3. Границы ошибок, которые рендерят поддерево заново
Идея пользователя GitHub Sorahn — граница ошибок, которая при возникновении ошибки просто снова рендерит тех же детей, — неплоха, но, к сожалению, любые компоненты в поддереве потеряют своё состояние. Для части случаев это может сработать, но это не универсальное решение, и если вы всё равно собираетесь адаптировать код, вероятно, лучше окружать TextNode элементами span.
Проблема: непредсказуемый event.target
Когда активен Google Переводчик, значение event.target становится непредсказуемым, потому что пользователи с высокой вероятностью кликают по одному из его элементов font, а не по исходному элементу, который вы, как разработчик, создали и вправе ожидать. В некоторых случаях, например внутри оверлеев, это может приводить к некорректной работе обработчиков кликов.
Хотя проблема довольно специфична и её относительно легко обойти, очень немногие разработчики вообще знают о ней или догадываются проверить такой сценарий.
Воспроизведение
В примере ниже текст на кнопке переводится эмулятором Google Переводчика. Когда вы кликаете где угодно внутри примера, тип элемента из event.target (по которому вы кликнули) появляется в тексте под кнопкой. Обычно при клике по кнопке event.target указывает на button, но с активным Google Переводчиком это будет элемент font.
Нажмите кнопку. Включите эмуляцию Google Переводчика. Нажмите ещё раз. Сравните результаты.
Демо доступно по ссылке.

Не только React
Вмешательство Google Переводчика затрагивает не только приложения на React.
Любой JavaScript-код, который схожим образом манипулирует DOM, оказывается под ударом. Сюда относятся операции вроде обновления значения TextNode, добавления или удаления дочерних элементов, а также использование event.target. Эти операции не специфичны для React.
Однако такие проблемы чаще замечают именно в приложениях на React, поскольку React активно использует «виртуальный DOM». Виртуальный DOM хранит ссылки на все узлы, чтобы обновлять только те части дерева, которые действительно изменились (процесс называется согласование, reconciliation). Это позволяет создавать высокопроизводительные приложения, поскольку такой подход эффективнее, чем замена узлов DOM. Поэтому использование виртуального DOM в React — с переиспользованием и обновлением узлов вместо их постоянной замены — стало естественной эволюцией фреймворков.
Не только Google Переводчик
Почти все машинные переводчики работают примерно так же, как Google Переводчик, так что проблема не ограничивается только им. Но масштаб еще шире: любые браузерные расширения, которые манипулируют DOM, могут мешать работе.
Вот несколько примеров:
Менеджеры паролей, которые изменяют формы, показывая выпадающие списки автозаполнения
Расширения, подставляющие альтернативные цены на сайтах конкурирующих интернет-магазинов (*)
Блокировщик рекламы, удаляющий элемент
AutocardAnywhere: показывает всплывающие изображения карт для коллекционных карточных игр (*)

Важно подчеркнуть, что я не считаю, будто команда Google Переводчика заслуживает упреков из-за этих проблем. Это отличный инструмент, который помогает людям по всему миру и делает веб доступным для гораздо большего числа пользователей. Архитектура Google Переводчика закладывалась в те времена, когда веб был совсем другим. Проблемы — это следствие эволюции: сайты уже давно не преимущественно статические; многие популярные сайты сегодня — это большие и сложные веб-приложения.
Исправить эти проблемы тоже непросто. Для многих переводов Google Переводчику нужно уметь перестраивать предложения, чтобы они звучали правильно на целевом языке. Сделать это почти невозможно, не вмешиваясь в DOM.
Решения пока нет (к сожалению)
На момент написания, увы, не существует решения, которое заставило бы Google Переводчик достаточно хорошо работать с React в крупном приложении. Как уже говорилось выше, обходные пути, устраняющие креши, вносят новые проблемы и все равно оставляют сложные приложения едва пригодными к использованию после перевода Google Переводчиком.
Есть пара вещей, которые вы можете сделать, но вряд ли они вам понравятся.
Печальный «фикс»
Когда я впервые столкнулся с этой проблемой в 2017 году, я написал в ишью трекер React, что «починил» свое приложение, полностью заблокировав перевод. Теперь, через 7 лет, с сожалением сообщаю, что это все еще выглядит единственным быстрым способом избежать всех проблем, вызываемых Google Переводчиком.
Мне не нравится решать это таким образом. Это делает приложения менее доступными для людей по всему миру. Но для некоторых сложных приложений это лучше, чем показывать пользователям Google Переводчика сломанную страницу, которая едва работает.
Если вы готовы вложить время и усилия, оборачивание условно рендерящихся TextNode в span устранит большую часть крешей (но не остальные проблемы). Обычно этого достаточно для простого сайта вроде этого: типичный сайт не слишком реактивен, у него небольшой кодбейс, над ним работает мало разработчиков, и он не показывает критически важные вычисляемые числа.
Вам придется внимательно взвесить, какое из решений лучше подойдет вашему приложению. Оставив Google Переводчик доступным, вы действительно поможете части пользователей, но придется повозиться с отладкой, чтобы добиться приемлемой работы и не показывать некорректные данные.
Что бы вы ни решили, может быть полезно заранее предупредить пользователей о возможных проблемах при использовании Google Переводчика. См. мою статью «How to detect Google Translate and other machine translation» о способе определить, активен ли Google Переводчик.
Альтернативы
Единственная альтернатива, которая приходит мне в голову, — реализовать собственную локализацию в приложении (то есть интернационализацию). Тогда машинный перевод не понадобится, а у международных пользователей будет максимально качественный опыт. Но у этого есть ряд минусов:
На это уходит очень много усилий
Сложно сделать правильно
Замедляет разработку
Хорошие переводчики стоят дорого
Нереалистично покрыть столько языков, сколько поддерживает Google Переводчик
В сумме это делает вариант не самым практичным для большинства приложений. Знаете ли вы о других альтернативах?
В React, возможно, существует внешний обходной путь, использующий механизм, похожий на «подсветку рендера» (render highlighting) в React Dev Tools, чтобы заставлять React перемонтировать весь родительский элемент для измененных
TextNode. Однако, изучив код этой функции, я увидел, что это часть файла более чем на 4500 строк, так что задача оказалась сложнее, чем я рассчитывал. Возможно, кто-то другой сможет на это посмотреть.
Заключение
На этом все о том, почему Google Переводчик роняет приложения на React (и другие веб-приложения). Или, как мы выяснили, обо всем, что связано с вмешательством в DOM со стороны сторонних браузерных расширений, которое мешает реактивности сложных JavaScript-приложений и часто приводит к крешам и другим проблемам.
Надеюсь, эта статья помогла разобраться в сути вопросов и выбрать подходящий способ работы с ними в вашем приложении.
Пожалуйста, помогите привлечь внимание к проблеме, проголосовав за баг-репорт в проекте Chromium: https://issues.chromium.org/issues/41407169. Дополнительное внимание повышает шансы, что проблему возьмут в работу.
Как вы считаете? Есть ли другие обходные пути? Знаете машинный переводчик, у которого нет этих проблем? Поделитесь мнениями по ссылкам ниже.
P.S. В приложении ниже я обсуждаю, насколько оправдано, что приложение претендует на полный и исключительный контроль над DOM, как это делает React со своим виртуальным DOM.
Русскоязычное JavaScript сообщество

Друзья! Эту статью перевела команда «JavaScript for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Frontend. Подписывайтесь, чтобы быть в курсе и ничего не упустить!
Дополнение: Должно ли приложение претендовать на полный контроль над своим DOM?
Один из разработчиков проекта перевода Localize поделился своим мнением в отчете Chromium по этой проблеме. Он написал:
Проблема возникает, когда библиотека JavaScript предполагает, что обладает полным и исключительным контролем над DOM (как, например, библиотека с VDOM), не учитывая, что DOM по своей природе задуман как изменяемый.
Даже если владелец сайта хочет отдать полный и исключительный контроль над DOM библиотеке с VDOM, на практике это невозможно, если у конечных пользователей установлены расширения Chrome, меняющие DOM (например, Grammarly, менеджеры паролей и т. п.), или если у пользователей браузеры, которые сами модифицируют DOM (например, встроенная функция перевода в Chrome). Существует большая экосистема, зависящая от манипуляций с DOM, — и она стала невольной жертвой распространения фреймворков с VDOM. У меня нет претензий к React или VDOM (я сам их использую и люблю), но идея менять Chromium в ответ на проблемы совместимости, возникающие с React, создает любопытный прецедент.
Это поднимает важный вопрос: должно ли приложение претендовать на полный контроль над DOM?
Как я уже упоминал, сторонние расширения мешают не только приложениям, которые претендуют на полный и исключительный контроль над DOM. Такие расширения, как Google Translate, затрагивают настолько много частей DOM, что могут ломать любые манипуляции с ним — даже небольшие фрагменты кода, не опирающиеся на библиотеку. Суть проблемы — в сторонних модификациях DOM. Она не ограничивается фреймворками, использующими виртуальный DOM.
Природа браузерных расширений (и моддинга в целом) предполагает внесение правок в чужую работу, и значительная часть ответственности — делать это так, чтобы ничего не сломать. Было бы неразумно ожидать от веб-разработчиков, что они будут учитывать и решать помехи со стороны сторонних (браузерных) расширений. Эти расширения находятся вне контроля веб-разработчиков и должны проектироваться с осторожностью, минимизируя возможные сбои. Ответственность за совместимость и отсутствие негативного влияния на веб-приложения лежит на создателях таких расширений.
Использование React виртуального DOM для переиспользования и обновления узлов вместо их постоянной замены — естественная эволюция фреймворков. В конце концов, это дает заметные преимущества по производительности. Поэтому я не считаю необоснованным, что React применяет этот подход ко всем узлам DOM, фактически претендуя на полный и исключительный контроль над DOM.
Учитывая все вышесказанное, именно сторонние расширения лучше всего способны оценить свой потенциальный эффект и устранить возможные помехи. В первую очередь именно им следует решать конфликты с корректной работой веб-приложений.
Но, возможно, также не вполне разумно ожидать, что расширения смогут предусмотреть все возможные типы вмешательства.
В итоге единственным по-настоящему разумным решением может быть устранение проблемы на уровне платформы — внутри самих браузеров, которые внедряют эти сторонние расширения. Вероятно, масштаб проблемы сейчас недостаточно велик, чтобы это произошло в ближайшее время, поэтому пока разработчикам расширений придется заботиться об этом самостоятельно.
Комментарии (3)

orekh
23.10.2025 08:56Читая первые пять абзацев одно и то же разными словами - а может не надо настолько сильно лить воду?
nin-jin
Единственное решение этой и сотни других проблем - переписать приложение на $mol, где таких проблем нет в принципе (самодеятельность расширения будет удалена при ререндере).
Kuch
Кто бы сомневался