Я написал много 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 ?

Комментарии (37)


  1. PrinceKorwin
    25.10.2024 13:20

    Почему-то говоря о производительности в подобных статьях упоминают только процессорное время. Тем временем чаще всего смена JS на перечисленное (Zig, Rust, C/C++,...) даёт наибольший выигрыш по памяти.


  1. AMDmi3
    25.10.2024 13:20

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


    1. ijustwanttobeacool
      25.10.2024 13:20

      Я в других экосистемах плохо разбираюсь, но как работают линтеры/сборщики в других системах? Для запуска линтера для других языков не нужно его скачивать из интернета и исполнять его на своей машине? Я без иронии, правда интересно :)


      1. Akorabelnikov
        25.10.2024 13:20

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


      1. AMDmi3
        25.10.2024 13:20

        Ставятся из системного репозитория как любой другой софт.


        1. ijustwanttobeacool
          25.10.2024 13:20

          Раскроете мысль как это защищает вас от запуска стороннего кода из интернета на своей машине? Я не защищаю npm и знаю о сотнях malware, которые случайным образом попадают на машину из-за огромного графа зависимостей и повсеместно используемых «крышечек» ^.

          Но вот я захожу в первый попавшийся репозиторий rust, вижу зависимости, вижу build.rs, который исполняется без какого либо sandbox окружения и не понимаю почему это не запуск стороннего кода и не дикость


  1. cheshirskins
    25.10.2024 13:20

    Здесь ещё вопрос в стоимости разработчика. На разницу месячной зарплаты между JS/TS и Golang разработчиками зачастую можно купить не самый плохой сервер, который, в подавляющем большинстве случаев, даже и не понадобится, или все будет упираться в БД, очередь сообщений и т.д.


    1. Vplusplus
      25.10.2024 13:20

      Не очень понятно, у кого из них зарплата больше. Вроде по грейдам должна быть приблизительно одинаковая.


      1. dom1n1k
        25.10.2024 13:20

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


        1. vkomp
          25.10.2024 13:20

          Я такой ;) Но чего-то особого спроса не наблюдаю. И так как сильно меньше матерюсь, когда в окне вижу го, то позиционируюсь дальше на него.


      1. cheshirskins
        25.10.2024 13:20

        Дело в компаниях, которые используют ту или иную технологию. Часто на Node.js пишут от безысходности, когда что-то написать на другом языке в приемлемый срок просто невозможно, или когда программисты способны писать только на JS. Соответственно язык зачастую выбирают не самые лучшие компании с не самыми хорошими условиями. Плюс есть определенная стигматизация в IT сообществе: бэкенд разработчики часто в целом презирают JS/TS и к попыткам использовать эти языки в родной среде относятся негативно. В итоге у Node.js разработчика не сильно большой выбор. По моим ощущениям обычно требуется middle-fullstack за 100к-150к.


        1. Arves
          25.10.2024 13:20

          Middle fullstack за 100к-150к? Это было актуально лет 10 назад, сейчас цифры в 2 раза больше.


    1. DoctorKrolic
      25.10.2024 13:20

      Разработчик на Go/Rust и.т.п. пишет код, который используется кучей других людей. Таким образом, ресурсы, затраченные единожды на разработку либы/cli-инструмента с лихвой покрываются экономией всех тех, кто их использует


  1. ko22012
    25.10.2024 13:20

    Признавайтесь, кто баловался с JIT & WebAssembly?

    Хотелось бы посмотреть реальные кейсы.


    1. polRk
      25.10.2024 13:20

      я оптимизировал graphql резолверы и маперы. оч сильно выручает


      1. cmyser
        25.10.2024 13:20

        А как ? Там кстати есть проблема того что при динамической сборке схемы нельзя у инпутов указать deprecated


      1. ko22012
        25.10.2024 13:20

        по скорости насколько выиграли? или по памяти


    1. exedealer
      25.10.2024 13:20

      недавно скомпилировал парсер sql из сорцов постгреса в wasm, чтобы в js можно было разделять sql скрипт на отдельные запросы. Это оказалось более подьемная задача чем портировать все это на js


  1. gun_dose
    25.10.2024 13:20

    И ни слова о том, что интерпретируемые языки всегда медленнее компилируемых.


    1. guihal
      25.10.2024 13:20

      Немного бред несёшь, js уже сто лет как интерпретируемо-компилируемый, так что иди почитай матчасть


      1. gun_dose
        25.10.2024 13:20

        интерпретируемо-компилируемый

        Лол, это же всё равно априори медленнее, чем полностью компилируемый язык. И ещё матчасть учить отправляют. Ну-ну)))


        1. PrinceKorwin
          25.10.2024 13:20

          Почему медленее? Только запуск и первые запросы будут медленее пока JIT не прожарит исходники а нейтив. Дальше работает уже машинный байткод.


          1. gun_dose
            25.10.2024 13:20

            Вот потому и медленнее, что есть всякие только. Не будем забывать про всякие CI/CD системы, где поднимается докер образ для сборки бандла, а после сборки образ удаляется. То есть код выполняется всего один раз. Тот самый раз, который медленный. И это по итогу влетает в копеечку за оплату облачных сервисов.


            1. PrinceKorwin
              25.10.2024 13:20

              Вы правы. Это действительно копеечка.


    1. netch80
      25.10.2024 13:20

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

      Но так как у JS сейчас чуть ли лучший JIT в мире, это сильно смягчено.


    1. ptr128
      25.10.2024 13:20

      Проблема не в том, что JS изначально был интерпретатором. Это как раз решаемо, так как компилировать можно и на лету. Причем не только кешируя результат, но еще и оптимизируя его в зависимости от профиля нагрузки.

      Проблема производительности JS в динамической типизации и сборке мусора. Первое в любом случае приводит к дополнительной нагрузке на процессор и память при исполнении кода. А второе может приводить к непредсказуемым задержкам в неподходящий момент времени и/или к излишнему потреблению памяти.

      Так как кеш CPU не резиновый, то бОльшее потребление памяти напрямую влияет на производительность.

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


  1. Soulskill
    25.10.2024 13:20

    Потому что железо позволяет вот и скептично. Запустить браузер на одноплатнике уже страдание. Фаза экстенсивного роста закончилась, теперь чтобы продолжать развитие надо занимать интенсивным развитием. Выжимать все соки из очевидно менее эффективного языка кажется не слишком рациональным.


  1. kivsiak
    25.10.2024 13:20

    Я вот на питоне перешел на uv с ruff и чую как мои волосы стали мягкими и шелковистыми. И это реальный прорыв в DX что со мной произошел за последние несколько лет.

    И когда я смотрю на то как вебпка сборки нашего фронта идут по 5-10 минут мне становится очень грустно и печально. Особенно когда вижу насколько быстрее работает bun и всякие swc с esbuild на моих петах. И мне как-то пофигу что я не могу эти тулзы поправить (на самом деле могу я go и rust знаю лучше js c ts) я их все равно за всю карьеру не патчил ни разу, как и 95% пользователей.


  1. AuToMaton
    25.10.2024 13:20

    А что, собственно, происходит? Как я понял, под предлогом ускорения инструменты написанные на JavaScript переписываются на других языках. Зачем? Не так велико ускорение чтобы его можно было считать причиной. Тогда зачем? Есть логические возможности…

    • обойти костность сообществ инструментов, переписывают чтобы сделать то, что не желают сделать на JavaScript

    • переписывание ради переписывания - про инструментарий JS не очень в курсе, но эпидемия переписывания на Rust (cat -> bat, mc -> nnn, node.js -> Tauri…) отлично рекламирует Rust, да и подводит к предыдущему пункту

    • переписывание ради ущемления JavaScript, он может тупо мешать, об этом ниже

    JavaScript движется в сторону JIT компиляции, а индустрия - в противоположную сторону, к W^X политике. Это плохо дважды - возможность применить JavaScript подрывает цели W^X политики, это раз, а когда обнаружится конфликт W^X и JIT - это лишнее недоумение, это два. И ведь это никак не домыслы, уже MIT, если правильно помню, диалект Scheme не планирует появляться на современных macOS - там тоже W^X. В Termux, которого самого постепенно выпиливают с Андроид, Racket есть, а Julia уже нет. И т.д. и т.п., лягушку варят не спеша.

    JavaScript представляет Web, а взаимоотношения Web с нативом… чисто для полноты решил упомянуть.

    Так что же происходит? ИМХО, конфликт интересов реализуется в действиях.


  1. avdosev
    25.10.2024 13:20

    Тем не менее я сомневаюсь, что JS медлителен от природы, или что мы исчерпали все возможности его улучшения.

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

    При этом как-то в статье не упоминается, что JS-приложения как-то неприлично много потребляют ОЗУ :( Вероятно, что даже среда разработчиков под JS-инфраструктуру ломанулась переписывать всё на Rust/любой другой язык именно из-за этого.

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


  1. idd451289
    25.10.2024 13:20

    Как по мне странно что разработчики движка не хотят выпилить cjs модели, отставив чисто esm, ибо тогда чисто физически анализировать и собирать код было бы проще

    К тому же куче я считаю странным что тс все ещё не находится в движке. Ибо по факту он стал стандартом. Причем таким нехитрым образом можно было бы проверки типов в рантайм

    Вот тогда бы зажили)


    1. artptr86
      25.10.2024 13:20

      К тому же куче я считаю странным что тс все ещё не находится в движке. Ибо по факту он стал стандартом.

      По факту он стал стандартом разработки, но сам язык не стандартизован. Далее, поскольку нодовский движок — это V8, то нужно, чтобы имплементацией Тайпскрипта занялся Гугл. Однако поскольку V8, в первую очередь, — браузерный движок, то нужно обеспечить ещё и интеграцию со стандартом HTML.

      Таким образом, у Гугла должна быть мотивация вложить кучу денег в имплементацию Тайпскрипта. Но без явных спецификаций успеть за Майкрософтом будет очень сложно. Отсутствие поддержки в движках от Мозиллы и Эппла также будет препятствовать проекту, тем более, что самому Гуглу Node.js не очень интересен.

      Причем таким нехитрым образом можно было бы проверки типов в рантайм

      нехитрым?


      1. idd451289
        25.10.2024 13:20

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

        По поводу спек и саппорта тоже согласен, но тут можно намекнуть на WECG, и к примеру потратить годик на стандарты

        нехитрым?

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


  1. ko22012
    25.10.2024 13:20

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


  1. alamat42
    25.10.2024 13:20

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

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

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

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

    Впрочем, если автор ратует за написание более производительного кода на js, то, может, было бы более конструктивно призвать js-коммьюнити писать не просто рабочий, но и производительный код, использующий все возможнлсти js по увеличению производительности? Тогда и поводов для переписывания на другие языки будет меньше. А конечные пользователи будут только выигрыше, ибо тормоза веба не нравятся никому.


  1. meded90
    25.10.2024 13:20

    Смотрел интервью с biomeJs он там сказал что основной буст получил изо того что просто выполняет все в максимально доступном числе потоков js так тоже может но в линтере этого нет. Второй буст в том что используется общее ast для разных инструментов я думаю в js тоже можно сделать такую оптимизацию


  1. kisskin
    25.10.2024 13:20

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

    По своему опыту с биржами - то, что сейчас реализовано через сайт, через десктопное приложение работало бы в десятки раз меньше потребляя память, в десятки раз менее нагружая процессор и десятки/сотни раз имея меньше глюков. Типичный пример - "десктопный" терминал Binance (как это модно сейчас на базе браузера), - когда я его запустил в первый раз, то удивился наличию кнопки "Обновить", через пару недель я понял, что без этой кнопки там работать просто невозможно. А так - страницы в хроме отжирающие по 4 Гб оперативки, грузящие по 2 потока на 100% (на i5 13600к), зависающие, с пропаданием получения данных - это становится массовым явлением, хотя с точки зрения выполняемой работы они делают меньше чем старый терминал Quik на Celeron 433, который работал просто на голову стабильней и быстрее.


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