Next.js отправляет мои console.log по WebSocket и это нельзя отключить
Недавно я потратил несколько часов на поиск причины серьёзной проблемы с производительностью в приложении на Next.js.
Сначала я подозревал повторные рендеры React, большие обновления состояния, трафик WebSocket или даже собственный код. Но профилировщик показал совсем другую картину.
Большая часть времени уходила на выполнение кода внутри:
node_modules/next/dist/next-devtools/userspace/app/forward-logs.js
А если точнее:
sendClientFileLogs()
Копнув глубже, я обнаружил, что Next.js перехватывает браузерные методы console и пересылает содержимое логов на сервер разработки.
Неожиданность
Как и многие разработчики, я предполагаю, что:
console.log(data)
просто выводит что-то в консоль браузера.
По всей видимости, теперь это уже не так.
В режиме разработки Next.js оборачивает методы console собственной реализацией:
const originalLog = console.log
console.log = (...args) => {
wrapper("log", ...args)
originalLog.apply(console, args)
}
Эта обёртка сериализует данные и отправляет их обратно на сервер разработки через WebSocket-соединение.
Звучит безобидно — пока приложение не начинает работать с большими объектами.
console.log на 65 МБ
В моём приложении есть крупные структуры данных, существующие во время выполнения.
Во время профилирования я обнаружил, что один-единственный вызов логирования приводил к сериализации и передаче примерно 65 МБ данных.
Не сохранению.
Не рендерингу.
Передаче.
По WebSocket.
Просто потому, что я залогировал объект, внутри которого находилось большое дерево состояния.
Результатом стали резкий рост нагрузки на CPU, огромные объёмы выделяемой памяти и почти непригодная для работы среда разработки.
На flame graph было хорошо видно, что Next.js тратит больше времени на пересылку логов, чем моё приложение — на реальную работу.
И что ещё хуже: отключение не работает
Очевидная реакция:
«Так просто отключи эту функцию».
Я тоже первым делом так подумал.
Я попробовал:
logging: false
Потом:
logging: {
browserToTerminal: false
}
Я изучил документацию.
Поискал в GitHub Issues.
Покопался в исходном коде.
Но console всё равно оставался обёрнутым.
Логи всё равно пересылались.
Трафик по WebSocket никуда не исчез.
Насколько я могу судить, сейчас нет надёжного и документированного способа вернуть нативное поведение браузерного console.
Обходное решение
Единственное, что действительно сработало, — полностью обойти Next.js.
Да, это некрасиво.
Да, это хак.
Но накладные расходы исчезли сразу:
setTimeout(() => {
if (typeof window !== 'undefined') {
// @ts-expect-error
global.console.log = global.console.log.__proto__
}
}, 0)
После применения этого обходного решения пересылка прекратилась, а производительность вернулась в норму.
Сам факт того, что такой хак сейчас работает надёжнее документированных настроек конфигурации, вызывает вопросы.
Проблема глубже
Проблема с производительностью раздражает.
Но куда сильнее меня беспокоит само архитектурное решение.
Переопределение фундаментальных браузерных API — очень серьёзное вмешательство со стороны фреймворка.
Если фреймворк перехватывает:
console.logconsole.warnconsole.error
и начинает передавать пользовательские данные по сети, у разработчиков должны быть:
- Чёткая документация.
- Явное включение этой функции.
- Гарантированная возможность её отключить.
Вместо этого многие разработчики, вероятно, обнаружат такое поведение только после того, как начнут профилировать проблемы с производительностью.
Фреймворки должны быть предсказуемыми
Главный урок этой истории вообще не про логирование.
Он про предсказуемость.
Когда я пишу:
console.log(obj)
я ожидаю вывод в консоль.
Я не ожидаю:
- сериализации;
- сетевого трафика;
- обмена данными по WebSocket;
- пересылки на сервер;
- дополнительной нагрузки на CPU.
Современные фреймворки постоянно добавляют удобные функции, но у каждого скрытого слоя магии есть своя цена.
Иногда эта цена измеряется растерянностью разработчиков.
Иногда — миллисекундами.
А иногда — 65-мегабайтными сообщениями по WebSocket, созданными одним-единственным вызовом console.log.