javascript

Почему я скептично отношусь к переписыванию JS-инструментов на «быстрых» языках

  • суббота, 26 октября 2024 г. в 00:00:04
https://habr.com/ru/companies/ruvds/articles/852694/

Я написал много JS-кода, и мне нравится JavaScript. Самое же главное, я выработал необходимые навыки для понимания, оптимизации и отладки этого кода, от которых не хочу отказываться.

Поэтому у меня вполне естественно возникает беспокойство по поводу охватившей сферу разработки одержимости переписывать каждый инструмент Node.js на быстрых языках вроде Rust, Zig, Go и прочих. Причём ценность этих языков я нисколько не преуменьшаю. Я даже принимал некоторое участие в разработке Servo, да и на столе прямо сейчас передо мной лежит книга по Rust. Но в целом основную часть своей карьеры я вложил в освоение всех нюансов JavaScript, и на данный момент этот язык для меня самый удобный.

Так что я признаю свою предвзятость (и, возможно, чрезмерный акцент на одном наборе навыков). Но, чем больше я об этом думаю, тем больше понимаю, что у моего скептицизма есть также обоснованные причины, о которых и пойдёт речь в текущей статье.

▍ Производительность


Одна из причин этого скептицизма в том, что, на мой взгляд, мы ещё не исчерпали все доступные возможности ускорения инструментов JS. Марвин Хейджмейстер прекрасно это продемонстрировал, показав, сколько всего доступно в ESLint, Tailwind и прочих инструментах.

В мире браузеров JS зарекомендовал себя как «достаточно быстрый» в отношении большинства рабочих нагрузок. Естественно, существует WebAssembly, но всё же будет верно сказать, что он используется преимущественно для нишевых задач, нагружающих процессор, а не для создания целых сайтов. Так почему же разработчики JS-инструментов для командной строки спешат отказаться от JavaScript?

▍ Глобальное переписывание


Я думаю, что разрыв в производительности возникает вследствие нескольких нюансов. Во-первых, есть вышеупомянутые доступные возможности, поскольку экосистема JS-инструментов достаточно долго фокусировалась на создании чего-то просто рабочего, а не чего-то быстрого. Теперь же мы достигли точки пресыщения, когда поверхность API практически полностью налажена, и все просто хотят «того же самого, но быстрее». Отсюда и взрыв разработки новых инструментов, которые являются, по большому счёту, упрощённой заменой существующих: Rolldown для Rollup, Oxlint для ESLint, Biome для Prettier и так далее.

Однако работают эти инструменты быстрее не обязательно из-за того, что написаны на более быстром языке. Их скорость может объясняться тем, что: 1) их пишут с упором на производительность и 2) поверхность API уже налажена, поэтому авторам не нужно тратить время на проработку общего дизайна. Чёрт возьми, да вам даже не требуется писать тесты! Просто используйте существующий набор от предыдущего инструмента.

За свою карьеру я часто видел, как в случаях, когда переписывание А в B приводит к повышению скорости, автор этого ремейка заявляет, что B быстрее А. Тем не менее, как отметил Райан Карниато, ремейк зачастую быстрее, просто потому что это ремейк — при переписывании чего-то вы знаете больше, больше внимания уделяете производительности и так далее.

▍ Байткод и JIT-компиляция


Вторая категория отрывов в производительности связана с теми возможностями, которые браузеры предоставляют нам бесплатно, и тем, что мы редко думаем о кэшировании байткода и JIT-компиляции.

Если при загрузке сайта второй или третий раз JS-код правильно кэширован, то браузеру не приходится больше парсить и компилировать исходный код в байткод. В таком случае он просто загружает последний напрямую с диска — это называется кэшированием байткода.

Более того, если функция «горячая» (часто выполняется), то она дополнительно оптимизируется в машинный код. Так работает JIT-компиляция.

В мире скриптов Node.js мы вообще не пользуемся преимуществами кэширования байткода. При каждом выполнении такого скрипта его весь приходится парсить и компилировать с нуля. И это существенная причина для упомянутой разницы в производительности между JS и не-JS инструментами.

Тем не менее, благодаря непревзойдённому Джойи Чунгу, в Node появилась функциональность «compile cache». Теперь вы можете настроить переменную среды и сразу получить ускорение загрузки скриптов Node.js:

export NODE_COMPILE_CACHE=~/.cache/nodejs-compile-cache

Я прописал её в файле ~/.bashrc на всех машинах для разработки. Надеюсь, однажды эта функциональность попадёт в базовую конфигурацию Node.

Что же касается JIT-компиляции, то это ещё одна полезная возможность, которую большинство скриптов Node, к сожалению, не использует. Сейчас вам нужно сперва выполнить функцию, чтобы она стала «горячей», поэтому на стороне бэкенда этот приём, скорее, подойдёт для долго работающих серверов, чем для одноразовых скриптов.

А ведь эта функциональность может значительно повлиять на скорость. В Pinaforce я подумывал о замене библиотеки blurhash на базе JS её версией на Rust (Wasm), пока не осознал, что к моменту, когда мы дошли до пятой итерации, разница в производительности исчезла. В этом заключается сила JIT-компиляции.

Возможно, в конечном итоге инструмент вроде Porffor можно будет использовать для AOT-компиляции скриптов Node. Ну а пока там, где нативные языки опережают JS, решением по-прежнему остаётся JIT.

Помимо прочего, должен признать, что производительность выигрывает при использовании Wasm вместо чисто нативных инструментов. Так что это может быть ещё одной причиной, по которой подобные инструменты стремительно заполоняют мир CLI, но не всегда браузерный фронтенд.

▍ Масштабность сообщества и удобство отладки


Я уже на это намекал, но именно здесь кроется основной источник моего скептицизма в отношении тенденции «переписать всё на нативных языках».

На мой взгляд, JavaScript — это язык рабочего класса. Он очень щадяще относится к типам (одна из причин, по которой я не большой поклонник TypeScript), его просто освоить (в сравнении с тем же Rust), и поскольку он поддерживается браузерами, с ним знакомо огромное число людей.

Годами в экосистеме JS очень многие авторы и пользователи библиотек активно использовали именно этот язык. Я думаю, что мы принимаем как должное те возможности, которые это открывает — например, в этом сообществе гораздо проще стать контрибьютором.

Процитирую Маттео Коллина:

«Большинство разработчиков игнорируют тот факт, что у них есть навыки для отладки/исправления/изменения используемых ими зависимостей. А ведь эти зависимости обслуживаются не какими-то полубогами, а такими же разработчиками».

И этот принцип рушится, если авторы библиотек JS используют другие (и более сложные) языки, и вот здесь они уже вполне могут перейти в ранг полубогов.

Ещё один нюанс: можно легко изменять зависимости JavaScript локально. Я часто корректировал что-то у себя в каталоге node_modules, когда искал какой-нибудь баг или работал над функциональностью библиотеки, от которой завишу. При этом, если она написана на нативном языке, мне приходится проверять исходный код и компилировать её самому, что является серьёзной преградой для начинающих.

Честно говоря, всё это стало уже довольно сложным в связи с повсеместным распространением TypeScript. Но TS не слишком далеко ушёл от базового JS, поэтому вы будете удивлены, сколько всего можете получить, кликнув «pretty print» в инструментах разработчика. К счастью, большинство библиотек Node тоже ещё не минифицированы.

Естественно, всё это также подводит нас к удобству отладки. Если я хочу отладить библиотеку JS, то могу просто использовать инструменты разработчика или знакомый мне отладчик Node.js. Я могу установить точки останова, просмотреть переменные и поразмышлять о чужом коде также, как делал бы это в случае своего. Да, это можно сделать и в случае Wasm, но потребуется уже другой набор навыков.

▍ Заключение


Думаю, здорово, что в экосистеме JavaScript зарождается новое поколение инструментов. Приятно видеть, как в итоге преображаются проекты вроде Oxc и VoidZero. Существующие решения действительно становятся всё медленнее, так что им наверняка не помешает конкуренция. (Особенно меня бесит типичный цикл линтинга-сборки: eslint + prettier + tsc + rollup).

Тем не менее я сомневаюсь, что JS медлителен от природы, или что мы исчерпали все возможности его улучшения. Порой я встречаю код, действительно акцентированный на скорости — например, недавние улучшения в Chromium DevTools с помощью чумовых техник вроде использования Uint8Array в качестве векторов битов. И я думаю, что мы раскрыли далеко не весь потенциал таких возможностей. (Если вы реально хотите получить комплекс неполноценности, то посмотрите другие коммиты Сета Бренита. Это просто улёт).

Ещё я думаю, что наше сообщество пока не до конца осознало, как будет выглядеть мир, если мы делегируем JS-инструменты элитной касте разработчиков на Rust и Zig. Могу представить, как средний JS-разработчик будет чувствовать себя абсолютно беспомощным при каждом возникновении в его инструменте сборки бага. Вместо того, чтобы подготовить будущее поколение разработчиков к достижению чего-то большего, мы можем, напротив, привить им выученную беспомощность. Представьте, каково будет обычному джуниору столкнуться с segfault вместо привычной для JavaScript Error.

Лично я уже занимаю должность старшего разработчика, так что, естественно, мне не пристало прятаться за ширмой безопасности JavaScript. Моя работа отчасти состоит в том, чтобы углубляться на несколько слоёв и выяснять, как работает каждый элемент технологического стека.

Тем не менее меня не покидает чувство, что мы движемся по неизвестному пути, сулящему непредвиденные последствия, в то время как есть другой, менее тяжкий путь, который может дать практически те же результаты. Хотя, судя по тому, что наш товарный поезд не показывает никаких признаков замедления, похоже, всё станет ясно, уже ближе к конечной станции…

Telegram-канал со скидками, розыгрышами призов и новостями IT 💻