Дисклеймер: самая большая ошибка в этой истории даже не выбор Dart, порядок действий. Вместо того чтобы в первый же день сделать честный raw benchmark на нашем production-like сценарии, я поверил в AOT, в статическую типизацию и в обещания ready for cloud - и сразу начал переносить сервис. Поэтому этот текст не только про Dart, но и про очень дорогой инженерный урок: сначала валидируй runtime-гипотезу, потом строй архитектуру вокруг неё. Чтобы не превращать текст в войну микрооптимизаций — все raw results, k8s manifests, CPU profiles, Dockerfiles и исходники рантаймов вынесены в репозиторий. Здесь цифры округлены. Я не гуру в Go, Dart и .NET — я обычный JS-разработчик. Поэтому вполне мог где-то напортачить. Увидели ошибку или способ улучшить код в рамках правил — милости прошу в PR.

Акт I. Надежда

Я работаю над SaaS-сервисом. Стек — Node.js. В целом, он устраивает всем: производительность на уровне, скорость разработки хорошая, один язык на всё. Да, некоторые сервисы жрали память как не в себя под нагрузкой. Например, наш OAuth2-сервис в пике потреблял 500 МБ, но это скорее показатель того, что нашим сервисом вообще пользуются :) Но главное токены выдавались, память возвращалась и деградация RPS упиралась в CPU на криптографии.

Но когда у тебя SaaS, сотни однотипных сервисов, и каждый потребляет по 80 МБ просто на старте — это уже другие цифры и цены на инфраструктуру.
Поэтому мы начали смотреть альтернативы. Ну как смотреть, из очевидного это Go, но команда у нас JS до мозга костей, так что Go энтузиазма не вызвал: err != nil на каждой строчке; context.Context первым аргументом в каждую функцию — Go way, explicit is better than implicit, и всё в этом роде. Ребята смотрели на это как на привет из 2009 года, когда Node.js точно также передавал err первым аргументом в каждый callback. Callback hell помнили хорошо. В Go этим до сих пор гордятся — ну и пусть гордятся, решили мы.

Начали искать что-то ещё и наткнулись на Dart. На бумаге: AOT-компиляция, статическая типизация, знакомый синтаксис и подходы. На официальном dart.dev прямым текстом: "Creating scalable, high performance APIs and event-driven apps are good use cases for Cloud Run". Не маркетинговый блог, не сторонняя статья — официальная документация, звучит как серебряная пуля, я и повёлся.

Знаете этот анекдот?
"Такой умный, красивый, успешный — интересно, почему от него бывшая ушла?"
И только через три месяца: "Таааак вооот почему..."
Вот примерно так это в итоге и сработало.

Мы понимали, что не получим Go-уровень. Но 10 МБ после старта вместо 80 - Claude обещал именно столько для наших типичных сервисов, начитавшись тех же маркетинговых блогов, что и я. Справедливости ради — простой HTTP сервер в AOT действительно занимал копейки. Я запустил, убедился, поверил. Статическая типизация, AOT, в 8 раз меньше памяти, избавление от чёрной дыры в виде node_modules - это звучит отлично. Как оказалось, только звучит.

Акт II. Отрицание

Проблема первая: экосистема

Мы решили переписать сервис авторизации на Dart как тестовый полигон, хорошая метрика для принятия решения. Плюс сам сервис - это только NestJS + самописная ORM для Redis + ts-oauth2-server, что при наличии Claude Code не выглядит как rocket science. И план казался действительно разумным: кодовая база на TypeScript с чёткой архитектурой — это именно тот материал, с которым ИИ-агент работает хорошо. Перенести структуру, сохранить логику, сменить рантайм. Легко?

Dart — это Flutter, для бэкенда нет ничего, от слова совсем. Аналога NestJS нет, Redis-клиентов можно пересчитать по пальцам, а что-то уровня ioredis — забудьте. Да, я знаю про Shelf и Serverpod, но Shelf - это Express.js уровня 2015 года, а Serverpod заточен под Flutter-команды, которые хотят full-stack на одном языке. Мы не пишем на Flutter, и Express нам не нужен.

Мне давно не нравилась модульная система NestJS и то, как там устроено дерево зависимостей. Знаю его болячки не понаслышке: за несколько лет я построил вокруг NestJs целый platform layer с JSON:API, JSON-RPC, SDK-клиентами, ACL и ORM-адаптерами (кому интересно, вот тут можно ознакомиться). Guard'ы, которые срабатывают раньше валидации данных, request scope у провайдеров, который ради одного request-scoped dependency пересоздаёт половину дерева, и вся эта магия, которая отлично продаётся на демках, но больно бьёт по большим системам. Поэтому мысль "раз в Dart NestJs нет — соберём свой NestDart, с блекджеком и куртизанками только сразу без этих проблем" в тот момент казалась почти естественной, а я уже мысленно прикидывал, сколько звёзд соберёт nestdart.

Тем более Max Plan у Claude Code и любовь к изобретению велосипедов, что может пойти не так? Ведь «План надёжен как швейцарские часы» (с)

Проблема вторая: сам Dart

Ещё в начале десятых помню хайп вокруг Dart, как Google продвигал его в замену JavaScript. Когда начал писать код, я понял, почему Google не смог.

Хочешь сделать for...in по объекту? Забудь. Хочешь вызвать статический метод у значения типа Type? Забудь. Type - это вообще какой-то огрызок, который я так и не понял. Передать можно, вызвать на нём ничего нельзя, в AOT это by design. Рефлексия тоже отсутствует, а как строить DI-контейнер без рефлексии?

Окей, думаю, может, просто надо принять евангелие от Dart и не пытаться писать TypeScript на Dart? Посмотрел, как вообще пишут. И тут меня накрыл ужас - кодогенерация. Нет, я не против неё, мы сами её используем, но то, как она реализована в Dart, — это тихий кошмар. Везде part 'file.g.dart' в исходниках, твой чистый файл уже содержит ссылку на сгенерированный. И два стула: либо коммитишь простыню в репозиторий, либо гоняешь генератор на каждый пайплайн. Я уже представил этот ад на ревью: автоматические апрувы, чтобы не читать всю простыню.

Ладно, принимаю евангелие от Dart. "AOT и 10 МБ" повторял как мантру.

Чтобы минимально трогать исходники, нашёл экспериментальный флаг --enable-experiment=enhanced-parts, изменялся только один файл, остальное генерировалось в сторонке. Собирать файл построчно и читать AST дерево я точно не хотел, поэтому Cli утилита. Для CLI-утилиты схема такая: запускаем входной файл в JIT-режиме → используем dart:mirrors → получаем типы с аннотациями → преобразуем ClassMirror в дескрипторы → вызываем сами аннотации → CLI генерирует нужные файлы. И это, чтобы сделать то, что в TypeScript/Java/C# делается тремя строками через рефлексию. Язык не даёт тебе инструмент: ты строишь инструмент, чтобы построить инструмент. Это и есть главная проблема: не то, что оно не работает, оно работало, но то, какой ценой, оставляет много открытых вопросов.

И вот парадокс: сам язык приятный. Писать код на Dart действительно приятно. Он логичный и, в целом, понятный. Большинство ошибок ловятся в процессе компиляции. Claude Code за несколько часов портировал ioredis — 4500 строк Dart против 23500 строк оригинального TypeScript. Язык настолько чистый, что даже ИИ пишет на нём лаконично. Но как только выходишь за пределы "просто писать код", начинается боль, а значит инструменты никто не пишет, а значит экосистемы нет. Это и объясняет, почему Dart за пределами Flutter скорее мёртв, чем жив. Доказательство этому появится чуть позже, с цифрами.

За 2 недели: NestJS-подобный фреймворк с древовидным DI, как в Angular, отдельная core-часть от транспорта, request scope через Zone, CLI для кодогенерации, как в лучших домах ЛондОна и Парижа (с). Claude Code портировал ioredis и нашу Redis ORM. В процессе переноса ts-oauth2-server как отдельного пакета для core части меня не покидало чувство, что слишком все "хорошо" выглядит. И вопрос "интересно, почему от него бывшая ушла" не оставлял меня в покое. Я решил проверить гипотезу в чистом виде. Через 2 недели. Молодец, что можно сказать.

Акт III. Гнев

Проблема третья: производительность

Решил провести простой нагрузочный тест. Намеренно простой сценарий: три эндпоинта, Postgres, Redis, одинаковые условия для всех рантаймов. Никакого NestJS, никаких ORM, сырой HTTP-сервер, чтобы сравнивать именно рантаймы, а не фреймворки.

Я ожидал, что Dart будет медленнее Go, но быстрее Node.js. Я даже репозиторий с бенчмарками назвал go_vs_dart, но цифры показали, что название явно нужно было другое.

Dart стартовал мгновенно и потреблял 3 МБ против 18 МБ у Node.js, не в 8 раз разница, но и зависимостей у nodejs почти не было. И сборка образа быстрей, и размер образа 5МБ, а не 50МБ, успокаивал я себя.

Но как только пошла нагрузка, картина сломалась.

По RPS Dart оказался значительно хуже Node.js, разрыв доходил до двукратного. Но RPS - это ещё полбеды: при 100m CPU и 500 VUS латентность p95 у Dart 9.5 секунды, У Node.js — 5 секунд, у Go — 2.9. Это уже не "медленнее", это timeout. В реальном продукте это означает не деградацию, а отказ.

С памятью отдельная история. В пике на полном CPU — одинаково, оба около 39 МБ, но стоит добавить throttling, Dart растёт до 47 МБ, Node.js остаётся на 37 МБ. GC под CPU pressure не успевает, и это все ещё не основная проблема. Dart отдавал обратно в ОС около 5%, дальше нет. Вообще. Ни байта. Нет, это не утечка, память доходила до максимума и больше не росла. Node.js после пиковой нагрузки скидывал обратно 80%.

В какой-то момент это уже перестало быть просто benchmark'ом и превратилось почти в торг с рантаймом. Я перепробовал все GC-флаги: --dontneed_on_sweep, --use_compactor, --force_evacuation, --mark_when_idle, --old-gen-heap-size, даже Dockerfile в какой-то момент стал выглядеть как ритуал надежды, а не конфигурация. Ничего не помогло. Dart VM team подтвердили - by-design. Dart VM спроектирован под Flutter — минимальные паузы GC для 60fps, а не минимальный RSS для Kubernetes. Для мобилки это правильное решение, для сервера же — смерть. Kubernetes не знает, что твой процесс "просто держит память на всякий случай". Он видит: pod занимает 40 МБ, значит ему нужно 40 МБ. Умножай на сотни сервисов, и экономия на старте в 15 МБ превращается в переплату на проде. А теперь вспомним про горизонтальное масштабирование: HPA поднял тебе 10 инстансов под пиком, окей, нагрузка спала, память не вернулась. Kubernetes смотрит на 40 МБ × 10 подов и не может уплотнить ноду. Node.js в той же ситуации сжался до 20 МБ × 10, и два пода уже освободили целую ноду. Dart платит за пик постоянно.

И это называется "ready for cloud". И это — на рантайме, который в официальной документации описан как "scalable, high performance APIs". На Cloud Run, где платишь за memory × time. Dart на Cloud Run буквально стоит дороже, чем Node.js: медленнее обрабатывает и дольше держит память после пика. Впрочем, Google виднее что у них high performance.

Последний штрих — обещанное доказательство с цифрами. Порт ioredis, написанный AI-агентом как механический один-в-один перенос, показал на 5% больше RPS, чем лучший Redis-клиент на pub.dev. Это не комплимент Claude Code, это некролог экосистеме: бэкенд-инфраструктуру на Dart никто серьёзно не писал, и это видно по цифрам.

Ну что, теперь понятно почему от него ушла бывшая...

Акт IV. Принятие

Я потратил 2 недели с надеждой найти альтернативу Node.js. После 2 недель разработки, собственного DI-фреймворка, порта Redis-стека и raw benchmark’ов стало окончательно ясно: для server-side workload’ов Dart сегодня — бумажный тигр. Очень хорош на бумаге, но на деле его ниша — только Flutter. Никаким ready for cloud тут даже не пахнет. Dart team убрала рефлексию ради AOT и маленького бинарника для мобилок. Несколько лет пытались внедрить макросы, но не смогли, взамен дали только сомнительную кодогенерацию. VM только с прицелом на тот же мобильник. И то, что будет делаться что-то в сторону реального ready for cloud, у меня большие сомнения.

Обидно, потому что Dart был в одном решении от того, чтобы стать серьёзным конкурентом, а при дальнейшем развитии и убийцей Node.js для cloud-native. Старт с 3 МБ, пик памяти как у Node.js. Если бы после спада нагрузки память возвращалась за разумное время, уже сейчас получился бы отличный цикл: HPA поднимает второй под за секунду, трафик распределяется, нагрузка падает, память освобождается, Kubernetes уплотняет ноды. Два пода по 20 МБ легко давали бы 2x RPS за те же ресурсы, на которые Node.js тратит один под с 40 МБ. Текущий разрыв по RPS уже не выглядел бы приговором. Но вместо этого под набрал 40 МБ навсегда. Одно архитектурное решение в GC в пользу 60fps, и весь сценарий рассыпался. Если бы память вела себя предсказуемо, появился бы backend use case, появилось бы комьюнити, подтянулось бы I/O ядро. Тогда Dart мог бы стать настоящей заменой Node.js для cloud-native.

И вот ирония - я искал серебряную пулю, которая уже есть. Да, она скучная, многословная, с err != nil на каждой строчке, и этот культ явности, где context.Context первым аргументом в каждую функцию звучит как привет из 2009 года. Но когда дело доходит до реального продакшена - сотни микросервисов, CPU-throttling в Kubernetes, network hop до баз и необходимость возвращать память после пиковой нагрузки — Go просто работает. Он даёт 1.5–3x больше RPS при жёстких лимитах, чем Node.js и Dart, и полностью восстанавливает память. Да, Bun native на полном CPU неожиданно хорош приближается и даже иногда обгоняет Go по RPS, возврат памяти - 90%, но при CPU throttling в 100m - рестарты. В Kubernetes, где throttling это норма, а не исключение, — это не production-ready. Так что на сегодняшний день, GO - это единственная разумная альтернатива, если ты не готов тратить время на тюнинг .NET или мириться с высокой памятью managed-рантаймов или сырым runtime в принципе. Поэтому я открываю https://go.dev/tour и начинаю с нуля. Следующая статья, возможно, будет именно про этот переход, уже без такой драмы.

Настоящий урок

Обычно в конце такого пишут: Dart — плохой, берите Go. Но мой урок другой. Комментаторы напишут: "автор дурак, два часа на k6 в первый день сэкономили бы две недели" и будут правы. Но есть ловушка, которую я осознал только в конце всей драмы, и она совсем не очевидная. Claude Code сделал механический перенос почти бесплатным — и именно в этом, как ни парадоксально, была ловушка, в которую я попал. Когда стоимость переноса падает до нуля, очень легко забыть, что стоимость неверной runtime-гипотезы остаётся прежней. ИИ-агент не спросит "а ты вообще проверил что этот рантайм тебе подходит?" Он просто очень быстро и очень качественно построит то, что ты попросил. Сначала валидируй гипотезу. Потом дай агенту строить.

P.S. Этот пост я написал за вечер, на эмоциях, с цифрами из первых замеров, а потом решил перепроверить, и провалился в бенчмарки ещё на 2 недели. Прогретые поды, throttling профили, нативные клиенты, compat layers, Bun, Deno, .NET.

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

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


  1. mayorovp
    13.04.2026 10:29

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

    Если что, у нас в C# кодогенераторы работают точно так же, единственное отлчие - имя файла со сгенерированным кодом вручную писать не надо.

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


    1. klerick Автор
      13.04.2026 10:29

      В Dart part 'file.g.dart' прописывается прямо в исходном файле, а это уже жесткая связка. Исходник знает о своём сгенерированном двойнике. Если бы генерация была полностью внешней - гоняй на пайплайне сколько угодно, исходники чистые, проверяешь результат. Но когда исходник содержит ссылку на то что сделал кодген - это уже другая история.


      1. mayorovp
        13.04.2026 10:29

        Всё ещё не вижу проблем. Ссылка на сгенерированный код в программе будет в любом случае (зачем-то же его генерировали!), чем же ссылка в форме имени файла фундаментально отличается от ссылки в форме имени функции?


        1. klerick Автор
          13.04.2026 10:29

          В Dart part 'file.g.dart' прописывается в исходнике до генерации. Без запуска генератора - файл невалидный, IDE ругается, компилятор не запустится. Да, генератор может сам добавить part 'file.g.dart' - но тогда либо коммитишь сгенерированный код в репозиторий (PR выглядит как простыня автогенерации), либо гоняешь генератор на каждом пайплайне как первый шаг перед компиляцией. Именно я и пытался обойти через  --enable-experiment=enhanced-parts и как в итоге оказалось что в целом с этим можно жить. но тот факт что это флаг экспериментальный тоже показатель


          1. mayorovp
            13.04.2026 10:29

            В Dart part ‘file.g.dart’ прописывается в исходнике до генерации. Без запуска генератора - файл невалидный, IDE ругается, компилятор не запустится.

            Значит, надо запустить генератор, чтобы файл появился.

            Да, генератор может сам добавить part ‘file.g.dart’ - но тогда либо коммитишь сгенерированный код в репозиторий (PR выглядит как простыня автогенерации), либо гоняешь генератор на каждом пайплайне как первый шаг перед компиляцией.

            Очевидно, part ‘file.g.dart’ не является сгенерированным кодом, даже если он был добавлен генератором. Относиться к этой сторочке как к сгенерированному коду - всё равно что отказываться добавлять в репозиторий package.json на том основании, что он был создан командой npm init.

            Кроме того, повторюсь: “нет ничего плохого в том, чтобы гонять генератор на каждый пайплайн”.


            1. klerick Автор
              13.04.2026 10:29

              Согласен, одна строчка не проблема. Проблема когда она в каждом файле где есть аннотация на классе. Стандартный build_runner именно так и работает - part 'user_service.g.dart' в user_service.dart, part 'user_controller.g.dart' в user_controller.dart и так далее. И это можно обойти только через экспериментальный флаг. И ещё один момент который меня удивил: аннотация не знает как генерировать код - она просто маркер. Логику генерации пишешь отдельно в Builder. То есть чтобы написать инструмент с кодогенерацией - нужно написать отдельный инструмент для генерации. Для меня как JS-разработчика это странно - аннотация должна сама знать что она генерирует.


              1. mayorovp
                13.04.2026 10:29

                аннотация должна сама знать что она генерирует

                Аннотации именно тем и отличаются от декораторов, что сами ничего не знают. Dart - не единственный язык, который использует аннотации, точно так же они работают как минимум в Java, C# и Go.


                1. klerick Автор
                  13.04.2026 10:29

                  Верно, аннотации в Java и C# тоже не знают что генерируют - но там есть рефлексия в рантайме, поэтому фреймворк сам читает аннотации и действует. В Dart рефлексии в AOT нет — поэтому и нужен отдельный Builder который парсит AST и генерирует код. Это и есть разница. В Go аннотаций вообще нет - там всё явно через код, другая парадигма.


                  1. mayorovp
                    13.04.2026 10:29

                    В Go есть теги на полях структур, это те же аннотации по сути

                    И в C# кроме рефлексии есть кодогенерация, управляемая теми же атрибутами. Кстати, активно работать над этой кодогенерацией начали как раз когда стали развивать AoT-компиляцию.


                    1. klerick Автор
                      13.04.2026 10:29

                      Справедливо про Go теги. Но все же это строки, анотации в dart это классы и декораторы это функции если говорить про js. но сути не меняют что теги что декораторы можно во время рантайма использовать, а анотации нет.
                      Над кодгеном начали активно работать после того как не смогли родить макросы. Теперь вместо part будет augment. Что могу сказать прогресс на лицо.


  1. SerafimArts
    13.04.2026 10:29

    Понимаю что немного оффтоп, но…

    И вот ирония - я искал серебряную пулю, которая уже есть.

    А помимо Go ещё есть C#, Java и PHP где последние два со своими Spring и Symfony уделывают вхлам всю экосистему и ноды и гошки вместе взятых под бекенд. Ну ладно, .NET тоже не такой уж и примитивный, там даже OAuth “из коробки”, но послабее будет.

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

    Всякие Elixir и Crystal в расчёт не беру (извините, языки-то реально хорошие), слишком маргинальные, почти как Dart. Найти на них разработчика будет тяжко. Ну и экосистема с первой тройкой (Java/C#/PHP) не сравнится.


    1. klerick Автор
      13.04.2026 10:29

      честно - мои знания php заканчиваются 5ой версии:). и я старался сравнить именно рантаймы, без фреймворков. Если у кого-то есть желание написать аналогичный сервис на PHP (там сейчас вроде и встроенный веб-сервер есть) или довести C#/.NET до ума, прогоню на том же стенде и добавлю в таблицу. .NET у меня заработал со второго раза, скорее всего что-то упустил — так что результаты там под вопросом. Rust — это была бы отдельная статья: "Как я потратил 2 недели чтобы написать Hello World"


      1. SerafimArts
        13.04.2026 10:29

        честно - мои знания php заканчиваются 5ой версии:). и я старался сравнить именно рантаймы, без фреймворков

        Ну акцент в статье был не только про рантайм, но и про экосистему в т.ч. Отсюда и это примечание на тему того, что тот же NestJS, который был упомянут – выглядит довольно блекло по сравнению с упомянутыми выше “энтерпрайз мастадонтами”. А сами языки как раз и рассчитаны на что-то более крупное.

        Взглянуть на тот же C#. Вряд ли кто-то вообще на нём так пишет (по крайней мере я не видел ничего похожего, без LINQ и прочих ништяков).


        1. klerick Автор
          13.04.2026 10:29

          Именно, экосистема - это и был первый звонок на который стоило обратить внимание раньше. Почему нет ничего нормального для бекенда? Ответ простой — потому что оно не работает. Aqueduct, Angel - умерли не потому что авторы были плохими разработчиками. Умерли потому что рантайм не подходит для сервера by design. Умные люди уже проверили до меня, просто я решил проверить ещё раз)


    1. klerick Автор
      13.04.2026 10:29

      Понимаю что немного оффтоп, но…

      я когда начал перепроверять числа в бечмарках. и стало все не так однозначно(с)
      Bun native - догонял и даже обгонял тот же Go. Но как добавляешь тротлинг или зависимости из npm становиться хуже голой nodejs или рестарты на хелсчеке. серебряная пуля тут скорей - "компромис"


  1. dkfbm
    13.04.2026 10:29

    Рефлексия тоже отсутствует, а как строить DI-контейнер без рефлексии?

    Вот этого совсем не понял. В моём мире использование рефлексии – антипаттерн. И DI прекрасно реализуется без неё.

    Хочешь сделать for...in по объекту? Забудь.

    И этого тоже. Если объект не имплементирует Iterable, то так и должно быть. А если имплементирует, то отлично работает.

    ____

    За тесты RPS – на бэкенде Дарт не пробовал, но звучит странно. Это же не Дарт как таковой работает, он компилируется в бинарный код платформы – что вроде бы должно давать максимальную производительность. Стоило бы выяснить, что там Клод наваял, а не тащить в прод не глядя. Или вы его в JS компилили?


    1. klerick Автор
      13.04.2026 10:29

      По рефлексии - я пришёл из JS-мира, где это норма: NestJS, Angular, TypeScript декораторы, и можно пробежаться по обьекту через for...in. Поэтому когда обнаружил что в Dart её нет - пошёл делать сам. В целом это не проблема, просто свои "нюансы" - как err != nil в Go. Каждый язык со своими особенностями.

      По RPS - Клод наваял порт ioredis, механический один-в-один перенос, и он показал на 5% больше RPS чем самый поплурный Redis-клиент. Так что за качество кода вопросов нет. За RPS - вопросы к Google, это их рантайм и их "ready for cloud". Ну и сервисы в репозитории - Node.js и Dart. Как говориться найдите 10 отличий. Код хуже тк его я писал сам)


      1. dkfbm
        13.04.2026 10:29

        я пришёл из JS-мира 

        Клод наваял порт ioredis

        Мне кажется, в этом и проблема ‒ Вы пытаетесь напрямую перенести привычки из JS в Дарт. А там таки сильно иначе всё. И GC тоже можно управлять в какой-то степени, прямо из кода. В частности, убедиться, что все объекты имеют реализацию dispose(), и что ссылки на них обнуляются после использования. Но это надо экспериментировать, конечно ‒ навскидку волшебную таблетку не предложу.


        1. klerick Автор
          13.04.2026 10:29

          Если пройдет по ссылки которую я указал в тексте

          Generally old space is organized in pages, so if there's at least one object on a page, the whole heap page is retained. So a partial reason may be fragmentation. Though if the analyzer is ever idle, it may trigger compacting GC, which can get rid of this fragmentation.

          вы хоть за вызывайте dispose() и забнуйляти переменные. если хоть что то осталось что вся страница останется


          1. dkfbm
            13.04.2026 10:29

            Если пройдет по ссылки которую я указал в тексте

            Вы уверены, что эта ссылка хоть сколько-то релевантна? Там речь о потреблении памяти сервером анализа кода в JIT среде. Как это связано с работой реального кода в AOT, я не знаю.


            1. klerick Автор
              13.04.2026 10:29

              Если почитать всю ветку внимательно - там речь о поведении VM, а не специфике JIT. VM одна и в JIT и в AOT. GC один, heap pages одни. JIT/AOT влияет на компиляцию кода, а не на то как VM управляет памятью.


              1. dkfbm
                13.04.2026 10:29

                Если почитать всю ветку внимательно - там речь о поведении VM, а не специфике JIT.

                Там речь о том, что Analyzer uses too much memory when there is many lints enabled. При чём тут боевой код?

                VM одна и в JIT и в AOT. 

                Нет. Тут ниже бурное обсуждение этого вопроса, повторяться не буду. Вкратце: JIT == VM, AOT == runtime. Разные вещи.


                1. klerick Автор
                  13.04.2026 10:29

                  AOT бинарник содержит embedded runtime: GC, event loop, isolates, I/O - без этого Dart код не запустится. JIT/AOT влияет только на компиляцию, runtime один. Если VM не отдаёт память и I/O тормозит под throttling и без - флаги не помогут, это задокументировано в репозитории с цифрами. Если я что-то готовлю неправильно - PR открыт, исходники там же.


                  1. dkfbm
                    13.04.2026 10:29

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

                    Ждать не нужно, мне есть, чем заняться. Впрочем, на пару исходников взглянул. Вот, например. Скажем, этот код зачем?

                    Object? _listToMap(Object? reply) {
                      if (reply is! List) return reply;
                      final map = <String, Object?>{};
                      for (var i = 0; i < reply.length; i += 2) {
                        map[reply[i].toString()] = reply[i + 1];
                      }
                      return map;
                    }

                    Почему не встроенный asMap()? Подозреваю, там такого порядочно, и это вносит свой вклад во время обработки. Ну и как бы в принципе характеризует качество кода.


                    1. SserjIrk
                      13.04.2026 10:29

                      Очевидно потому что asMap() всегда вернет Map<int, ЧтоТо> а этот метод возвращает Map<String, Object?>, т.е. трансформирует ["key1", value1, "key2", value2...] в {"key1": value1, "key2": value2 ...}


                      1. dkfbm
                        13.04.2026 10:29

                        Очевидно потому что asMap() всегда вернет Map<int, ЧтоТо> а этот метод возвращает Map<String, Object?>

                        Логично, вопрос снимается.


                    1. klerick Автор
                      13.04.2026 10:29

                      Любопытно — скинули кусок кода который написал AI агент, и который в итоге оказался быстрее лучшего Redis клиента на pub.dev. Вот оригинал в ioredis на TypeScript — та же логика, тот же шаг 2. Про бенчмарки — это написано в статье, которую вы комментируете: "Никакого NestJS, никаких ORM, сырой HTTP-сервер, чтобы сравнивать именно рантаймы, а не фреймворки". Но зачем читать, когда можно сразу комментировать?


                      1. dkfbm
                        13.04.2026 10:29

                        Но зачем читать, когда можно сразу комментировать?

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


                      1. klerick Автор
                        13.04.2026 10:29

                        • Память не течёт — растёт до пика и стабильно останавливается

                        • После снятия нагрузки возвращает ~5%

                        • VM не освобождает heap page пока жив хоть один объект

                        • а если и освобождает то в idle, которые наступает на сервере практически никогда

                        • GC флаги которые смог найти не помогают

                        • код заведом простой, сервис такой же запрос - база - ответ

                        Да я уверен что Dart виноват и решения нет. Если есть, я буду только рад. Ткните носом чего я еще не видел за эти две недели)


                      1. dkfbm
                        13.04.2026 10:29

                        • VM не освобождает heap page пока жив хоть один объект

                        Никогда такого не было, и вот опять ©

                        Ну нет никакой VM в AOT. GC есть, да. И если он видит свободную память, то чего бы не скушать. Вам действительно нужны эти большие мегабайты, буквально под каждый запрос? Если нет, то решением могло бы быть ограничить доступную память для докера ‒ как только она начнёт заканчиваться, GC начнёт её чистить.


                      1. klerick Автор
                        13.04.2026 10:29

                        Я просто оставлю это здесь. Называйте как хотите - VM, runtime, встроенный GC, хоть Осликом Федя. Результат в таблице один и тот же. Ограничение памяти для пода - последняя строка таблицы: cgroup memory.limit = 40Mi → OOMKill → restart at 7Mi. Kubernetes перезапускает под вместо того чтобы освободить память.

                        И да - мне не нужны эти мегабайты под каждый запрос. Мне нужно чтобы они возвращались после того как нагрузка спала. Именно об этом написано в статье которую вы комментируете.

                        А еще в статье написано что VM, runtime, встроенный GC, или Ослик Федя ужасный тормоз на обычном сценарий для вебсервиса


    1. SserjIrk
      13.04.2026 10:29

      Это же не Дарт как таковой работает, он компилируется в бинарный код платформы – что вроде бы должно давать максимальную производительность

      Дарт просто собирает нативный образ DartVM + ваш код в виде натива, который может запустить система. А по сути это та же самая а-ля javaVM, только маленькая.


      1. dkfbm
        13.04.2026 10:29

        Ну очень маленькая:

        AOT Compilation (Independent Binary)

        ​If you use the dart compile exe command, the code runs as an independent, native executable.

        • How it works: Dart uses Ahead-of-Time (AOT) compilation to convert your source code into machine code (x64 or ARM64) specific to Linux.

        • The "VM" question: It does not run inside a separate, standalone Virtual Machine like a Java .jar file does. Instead, the "Dart Runtime" is stripped down and embedded directly into the binary.

        • Runtime contents: This embedded runtime is minimal, handling only essential tasks like garbage collection (GC) and the basic memory management required for Dart's "Isolate" model.

        • Result: You get a self-contained file that you can run on a Linux machine without needing the Dart SDK installed.


        1. SserjIrk
          13.04.2026 10:29

          И что тут опровергает "образ DartVM + ваш код"?

          Именно DartVM работает с память, обслуживает ваши изоляты (ведь main запускается в изоляте). Абсолютно то же самое делает и java. Только Java по ходу исполнения переводит байт код в нативные (прогрев) и в теории может что-то оптимизировать под конкретное железо. А Dart это сделает сразу на "усредненую" машину.

          Минимальный exe "Hello world" который собирает Dart под Windows - 5Mb. Если бы он собирал несколько килобайт как это делают C/C++ можно было бы рассуждать о неком настоящем нативе.


          1. dkfbm
            13.04.2026 10:29

            И что тут опровергает "образ DartVM + ваш код"?

            Runtime != VM. Код, написанный на чистом С тоже использует свой рантайм, например.


            1. SserjIrk
              13.04.2026 10:29

              Все деления Runtime/VM - чистый маркетинговый флуд. Ну или нужно ставить VM отдельно / она включена в ваш код.

              Я собираю один единый exe с помощью GraalVM для десктоп на JavaFX.
              Приложение полностью независимо, один exe не требует никаких предустановленных компонентов на машине.
              Но это все та-же JavaVM только находится внутре того-же exe.
              Это теперь Runtime или VM?


              1. dkfbm
                13.04.2026 10:29

                Все деления Runtime/VM - чистый маркетинговый флуд.

                Почему? Разница существенная, виртуалка ‒ это именно что отдельная среда исполнения внутри host OS, а рантайм ‒ просто кусок кода, подключающийся по необходимости и крутящийся внутри самой ОС.


                1. SserjIrk
                  13.04.2026 10:29

                  Код ни в Dart ни в Java не запускается сам по себе. Его запускает JavaVM или DartVM. В случае Dart это тот самый Loop который строит очередь тасков/микротасков и построчно выполняет каждый шаг программы. Как не обзовите НативнаяКомпиляция/Предкомпиляция - все равно каждое действие проходит через очередь DartVM. Она рулит потоком управления.


                  1. dkfbm
                    13.04.2026 10:29

                    В случае Dart это тот самый Loop который строит очередь тасков/микротасков и построчно выполняет каждый шаг программы. 

                    Ммм, а где не так? В любой программе есть event loop, который так работает. Дарт ни разу не исключение. И это сильно отличается от запуска внутри изолированной среды VM/application server ‒ которая вносит свои накладные расходы.


                    1. SserjIrk
                      13.04.2026 10:29

                      Ммм, а где не так?

                      В реальном нативе, тот же C/C++. Ваша программа набор инструкций процессору. Которые он будет выполнять без остановки, пока они не закончатся. И нет в собранном бинарнике никаких Loop. Все инструкции идут шаг за шагом без остановок. Ну только системный планировщик их приостанавливает, рулит их приоритетом.
                      Ваш код Dart это не иснтрукции процессору а инструкции DartVM. Хоть они и уже в бинарном виде. Но запустит их именно DartVM в виде своих task/microtask. И он может менять их порядок выполнения для всяких await/thien.


                      1. dkfbm
                        13.04.2026 10:29

                        В реальном нативе, тот же C/C++. Ваша программа набор инструкций процессору. Которые он будет выполнять без остановки, пока они не закончатся. И нет в собранном бинарнике никаких Loop.

                        Эмм, я дико извиняюсь, но это так работает только в fire-and-forget утилитах. Как только появляется хоть минимальная интерактивность ‒ а все сервисы/демоны работают именно так, без главного цикла никуда, иначе программа завершится сразу после запуска и не сможет обрабатывать внешние события. Ежели чего, С/С++ ‒ чуть ли не единственные (после ассемблера) языки, на которых я писал код за деньги. И главные циклы там обязательно присутствовали.


                      1. SserjIrk
                        13.04.2026 10:29

                        Вы говорите о том что можете сделать в нативе. В конце концов DartVM сама написана на C++ и при желании можно хоть повторить ее в своем коде.
                        Но код Dart сам по себе никогда не может быть так запущен. Он оборачивается в task и встает в очередь EventLoop.


                      1. mayorovp
                        13.04.2026 10:29

                        А у этого EventLoop какие-нибудь наблюдаемые следствия есть, или тут важно просто поспорить?


                      1. SserjIrk
                        13.04.2026 10:29

                        Конечно есть - накладные расходы.
                        Собственно этот обмен мнениями и начался с цитаты про нативник dart:
                        ".. он компилируется в бинарный код платформы – что вроде бы должно давать максимальную производительность.."


                      1. dkfbm
                        13.04.2026 10:29

                        Но код Dart сам по себе никогда не может быть так запущен. Он оборачивается в task и встает в очередь EventLoop.

                        По-моему, мы пошли по кругу. Уже писал ‒ event loop есть неотъемлемая часть любой интерактивной программы, на любом языке.


                      1. SserjIrk
                        13.04.2026 10:29

                        Ну в итоге ответ на ".. что вроде бы должно давать максимальную производительность..." - по тестам Dart код проигрывает в производительности нативному C++ в 2-3 раза.
                        Это согласно тестам в которых использовались оба языка:
                        Dart vs C++


                      1. dkfbm
                        13.04.2026 10:29

                         по тестам Dart код проигрывает в производительности нативному C++ в 2-3 раза.Это согласно тестам в которых использовались оба языка

                        Мы сменили тему? Довольно трудно спорить с тем, что хорошо оптимизированный код на С/С++ будет быстрее в большинстве случаев. Но это никак не связано с тем, работает ли Дарт под VM или просто задействует свой рантайм.


                      1. SserjIrk
                        13.04.2026 10:29

                        Я повторюсь еще раз - единственная разница между VM языка и Runtime только в том что с VM вы берете свой файл и без изменений его переносите между linux/windows/ЧемТоЕще. Или прибиваете гвоздями в свой готовый файл и собираете уже отдельно для linux/windows/ЧтоТоЕще. .Net runtime, Node.js - это тоже runtime только вот никакой разницы с JavaVM у них нет. И Dart runtime точно такой же, просто он включен в вашу программу и не требует установки отдельно.


                      1. SserjIrk
                        13.04.2026 10:29

                        Пришлось сходить на официальный сайт чтобы поставить точку:
                        dart compile exe
                        A standalone, architecture-specific executable file containing the source code compiled to machine code and a small Dart runtime.
                        Идем по ссылке что такое Dart runtime:
                        On native platforms, the Dart runtime is automatically included inside self-contained executables, and is part of the Dart VM provided by the dart run command.
                        Вот и весь рантайм - всего лишь обрезанная часть той самой DartVM. Без функций оптимизации и динамической загрузки.