Transcript

00:00:00(бодрая музыка) - Отлично, всем спасибо, здравствуйте.
00:00:07Меня зовут Люк Сандберг.
00:00:09Я инженер-программист в Vercel, работаю над Turbo Pack.
00:00:12Я работаю в Vercel около шести месяцев,
00:00:14и этого времени мне как раз хватило,
00:00:16чтобы выйти на эту сцену и рассказать вам обо всей той замечательной работе,
00:00:21которую я не делал.
00:00:23До Vercel я работал в Google,
00:00:25где занимался нашими внутренними веб-инструментами и делал странные вещи,
00:00:30например,
00:00:31создавал компилятор TSX в байт-код Java и работал над компилятором Closure.
00:00:37Так что,
00:00:38когда я пришел в Vercel,
00:00:39это было похоже на попадание на другую планету,
00:00:43все было по-другому.
00:00:45И я был очень удивлен всем,
00:00:47что мы делали в команде,
00:00:49и нашими целями.
00:00:50Сегодня я поделюсь несколькими проектными решениями,
00:00:53которые мы приняли в Turbo Pack,
00:00:54и расскажу,
00:00:55как,
00:00:55по моему мнению,
00:00:56они позволят нам продолжать развивать ту фантастическую производительность,
00:01:00которую мы уже имеем.
00:01:01Чтобы это обосновать, вот наша общая цель проектирования.
00:01:06Из этого сразу можно сделать вывод,
00:01:09что мы,
00:01:09вероятно,
00:01:10приняли несколько трудных решений.
00:01:14Итак, что насчет холодных сборок?
00:01:17Они важны,
00:01:18но одна из наших идей заключается в том,
00:01:20что вы вообще не должны с ними сталкиваться.
00:01:22И именно на этом будет сосредоточен этот доклад.
00:01:24В основном докладе вы немного слышали о том,
00:01:27как мы используем инкрементальность для улучшения производительности сборки.
00:01:31Ключевая идея,
00:01:32которую мы используем для инкрементальности,
00:01:34связана с кэшированием.
00:01:35Мы хотим сделать кэшируемым буквально все,
00:01:37что делает сборщик,
00:01:38чтобы при каждом изменении нам приходилось переделывать только работу,
00:01:41связанную с этим изменением.
00:01:43Или,
00:01:43возможно,
00:01:44другими словами,
00:01:45стоимость вашей сборки должна масштабироваться в зависимости от размера или сложности вашего изменения,
00:01:51а не от размера или сложности вашего приложения.
00:01:53И именно так мы можем гарантировать,
00:01:55что Turbo Pack будет продолжать обеспечивать разработчикам хорошую производительность,
00:01:59независимо от того,
00:01:59сколько библиотек иконок вы импортируете.
00:02:01Итак,
00:02:02чтобы помочь понять и обосновать эту идею,
00:02:04давайте представим самый простой в мире сборщик,
00:02:07который,
00:02:08возможно,
00:02:08выглядит так.
00:02:09Итак, вот наш «малыш»-сборщик.
00:02:12И это,
00:02:12возможно,
00:02:13немного слишком много кода для одного слайда,
00:02:15но дальше будет хуже.
00:02:17Итак, здесь мы парсим каждую точку входа.
00:02:20Мы отслеживаем их импорты,
00:02:21разрешаем их ссылки рекурсивно по всему приложению,
00:02:25чтобы найти все,
00:02:26от чего вы зависите.
00:02:28Затем,
00:02:28в конце,
00:02:29мы просто собираем все,
00:02:30от чего зависит каждая точка входа,
00:02:32и помещаем это в выходной файл.
00:02:35Ура, у нас есть «малыш»-сборщик.
00:02:38Очевидно,
00:02:39это наивно,
00:02:39но если мы посмотрим на это с инкрементальной точки зрения,
00:02:42ни одна часть этого не является инкрементальной.
00:02:45Так что мы определенно будем парсить определенные файлы по несколько раз,
00:02:49возможно,
00:02:50в зависимости от того,
00:02:51сколько раз вы их импортируете,
00:02:52это ужасно.
00:02:53Мы определенно будем разрешать импорт React сотни или тысячи раз.
00:02:57Так что, знаете, больно.
00:03:01Итак,
00:03:01если мы хотим,
00:03:02чтобы это было хотя бы немного более инкрементальным,
00:03:05нам нужно найти способ избежать избыточной работы.
00:03:08Итак, давайте добавим кэш.
00:03:10Итак, вы можете представить, что это наша функция парсинга.
00:03:15Она довольно проста.
00:03:15И она,
00:03:16вероятно,
00:03:16является своего рода рабочей лошадкой нашего сборщика.
00:03:19Знаете, очень просто.
00:03:19Мы читаем содержимое файла,
00:03:22передаем его SWC,
00:03:23чтобы получить AST.
00:03:25Итак, давайте добавим кэш.
00:03:27Хорошо, это, очевидно, приятная и простая победа.
00:03:31Но,
00:03:32знаете,
00:03:32я уверен,
00:03:33что некоторые из вас уже писали код для кэширования.
00:03:36Возможно, здесь есть некоторые проблемы.
00:03:38Например, что, если файл изменится?
00:03:41Это, очевидно, то, что нас волнует.
00:03:46И,
00:03:46знаете,
00:03:47что,
00:03:47если файл на самом деле не файл,
00:03:50а три символические ссылки в плаще?
00:03:52Многие менеджеры пакетов организуют зависимости именно так.
00:03:55И мы используем имя файла в качестве ключа кэша.
00:03:59Этого достаточно?
00:04:00Например, мы собираем для клиента и сервера.
00:04:03Одни и те же файлы попадают и туда, и туда.
00:04:04Это работает?
00:04:05Мы также храним AST и возвращаем его.
00:04:08Так что теперь нам нужно беспокоиться о мутациях.
00:04:11Так что,
00:04:11знаете,
00:04:12и наконец,
00:04:13разве это не очень наивный способ парсинга?
00:04:16Я знаю,
00:04:16что у всех есть огромные конфигурации для компилятора.
00:04:21Например, что-то из этого должно попасть сюда.
00:04:23Так что, да, это все отличные замечания.
00:04:27И это очень наивный подход.
00:04:32И на это, конечно, я бы сказал: да, это не сработает.
00:04:36Так что же нам делать, чтобы исправить эти проблемы?
00:04:39Пожалуйста, исправьте и не делайте ошибок.
00:04:44Итак, хорошо.
00:04:46Так что, возможно, это немного лучше.
00:04:49Знаете,
00:04:49здесь вы видите,
00:04:50что у нас есть некоторые преобразования.
00:04:52Нам нужно выполнять индивидуальные действия с каждым файлом,
00:04:55например,
00:04:55понижать уровень или реализовывать использование кэша.
00:04:58У нас также есть некоторая конфигурация.
00:05:00И поэтому,
00:05:01конечно,
00:05:01нам нужно включить это в наш ключ для кэша.
00:05:04Но, возможно, вы сразу же подозрительны.
00:05:08Например, это правильно?
00:05:09Например,
00:05:10достаточно ли на самом деле идентифицировать преобразование по имени?
00:05:13Не знаю, возможно, у него есть своя сложная конфигурация.
00:05:16И,
00:05:17хорошо,
00:05:17и,
00:05:18например,
00:05:19это значение JSON действительно охватят все,
00:05:23что нас волнует?
00:05:24Будут ли разработчики поддерживать это?
00:05:26Насколько большими будут эти ключи кэша?
00:05:29Сколько копий конфигурации у нас будет?
00:05:31Так что я лично видел код именно такой,
00:05:33и мне кажется,
00:05:34что его почти невозможно осмыслить.
00:05:37Хорошо,
00:05:37мы также попытались решить эту другую проблему,
00:05:40связанную с инвалидациями.
00:05:43Итак, мы добавили API обратного вызова для чтения файла.
00:05:46Это отлично,
00:05:47так что если файл изменится,
00:05:49мы можем просто удалить его из кэша,
00:05:51чтобы не продолжать выдавать устаревшее содержимое.
00:05:55Хорошо,
00:05:55но это на самом деле довольно наивно,
00:05:57потому что,
00:05:57конечно,
00:05:58нам нужно удалить наш кэш,
00:05:59но наш вызывающий объект также должен знать,
00:06:01что ему нужна новая копия.
00:06:03Итак, хорошо, давайте начнем передавать колбэки.
00:06:06Хорошо, мы это сделали.
00:06:09Мы передали колбэки вверх по стеку.
00:06:12Здесь вы видите,
00:06:13что мы позволяем нашему вызывающему объекту подписываться на изменения.
00:06:16Мы можем просто перезапустить всю сборку,
00:06:18если что-то изменится,
00:06:20и если файл изменится,
00:06:21мы вызываем это.
00:06:22Отлично, у нас есть реактивный сборщик.
00:06:25Но это все еще едва ли инкрементально.
00:06:28Так что,
00:06:29если файл изменится,
00:06:31нам нужно снова пройтись по всем модулям и сгенерировать все выходные файлы.
00:06:37Так что,
00:06:38знаете,
00:06:38мы сэкономили кучу работы благодаря нашему кэшу парсинга,
00:06:43но этого на самом деле недостаточно.
00:06:45И затем наконец, есть вся эта другая избыточная работа.
00:06:49Например, мы определенно хотим кэшировать импорты.
00:06:52Мы можем находить файл множество раз,
00:06:53и нам постоянно нужны его импорты,
00:06:55поэтому мы хотим поместить туда кэш.
00:06:57И,
00:06:58знаете,
00:06:58результаты разрешения на самом деле довольно сложны,
00:07:01поэтому мы определенно должны кэшировать их,
00:07:03чтобы мы могли повторно использовать работу,
00:07:05которую мы проделали при разрешении React.
00:07:08Но, хорошо, теперь у нас есть другая проблема.
00:07:11Ваши результаты разрешения меняются,
00:07:13когда вы обновляете зависимости или добавляете новые файлы,
00:07:16поэтому нам нужен еще один колбэк там.
00:07:18И мы определенно также хотим,
00:07:19например,
00:07:20кэшировать логику для генерации выходных данных,
00:07:23потому что,
00:07:23если подумать,
00:07:24в сессии HMR вы редактируете одну часть приложения,
00:07:27так почему мы каждый раз переписываем все выходные данные?
00:07:31И также,
00:07:32вы можете,
00:07:32например,
00:07:33удалить выходной файл,
00:07:34поэтому нам,
00:07:35вероятно,
00:07:36следует отслеживать изменения и там.
00:07:39Хорошо,
00:07:39возможно,
00:07:40мы решили все эти проблемы,
00:07:42но у нас все еще есть эта проблема: каждый раз,
00:07:45когда что-то меняется,
00:07:46мы начинаем с нуля.
00:07:48Так что,
00:07:48своего рода,
00:07:49весь поток управления этой функцией не работает,
00:07:51потому что,
00:07:52если изменится один файл,
00:07:53мы бы действительно хотели перескочить в середину этого цикла `for`.
00:07:56И наконец,
00:07:57наш API для нашего вызывающего объекта также безнадежно наивен.
00:08:03Они,
00:08:03вероятно,
00:08:04на самом деле хотят знать,
00:08:05какой файл изменился,
00:08:05чтобы они могли,
00:08:06например,
00:08:07отправлять обновления клиенту.
00:08:07Так что, да.
00:08:11Так что этот подход на самом деле не работает.
00:08:13И даже если бы мы каким-то образом передали все колбэки во все эти места,
00:08:17как вы думаете,
00:08:18вы смогли бы на самом деле поддерживать этот код?
00:08:21Как вы думаете,
00:08:22вы смогли бы,
00:08:23например,
00:08:23добавить в него новую функцию?
00:08:24Я нет.
00:08:25Я думаю, это просто рухнет и сгорит.
00:08:28И, знаете, на это я бы сказал: да.
00:08:34Итак, еще раз, что нам делать?
00:08:36Знаете,
00:08:37как и при общении с LLM,
00:08:39сначала нужно знать,
00:08:41чего вы хотите.
00:08:43А затем вы должны быть предельно ясны в этом.
00:08:48Итак, чего же мы вообще хотим?
00:08:50Итак,
00:08:50знаете,
00:08:51мы рассмотрели множество различных подходов,
00:08:54и многие люди в команде на самом деле имели большой опыт работы со сборщиками.
00:08:59Итак, мы пришли к этим своего рода общим требованиям.
00:09:02Итак,
00:09:02мы определенно хотим иметь возможность кэшировать каждую дорогую операцию в сборщике.
00:09:05И это должно быть очень легко сделать.
00:09:08Например,
00:09:09вы не должны получать 15 комментариев к своему обзору кода каждый раз,
00:09:11когда добавляете новый кэш.
00:09:12И я на самом деле не очень доверяю разработчикам писать правильные ключи кэша или отслеживать входы или зависимости вручную.
00:09:24Так что мы должны это обрабатывать.
00:09:26Мы определенно должны сделать это надежным.
00:09:30Далее, нам нужно обрабатывать изменяющиеся входы.
00:09:33Это большая идея в HMR, но даже между сессиями.
00:09:36Так что,
00:09:36в основном,
00:09:37это будут файлы,
00:09:37но это также могут быть такие вещи,
00:09:39как настройки конфигурации.
00:09:40А с кэшем файловой системы это на самом деле оказывается такими вещами,
00:09:43как переменные окружения,
00:09:44тоже.
00:09:45Итак, мы хотим быть реактивными.
00:09:47Мы хотим иметь возможность пересчитывать вещи,
00:09:50как только что-то изменится,
00:09:52и мы не хотим передавать колбэки повсюду.
00:09:54Наконец,
00:09:55нам просто нужно использовать преимущества современных архитектур,
00:09:59быть многопоточными и просто быстрыми в целом.
00:10:02Итак,
00:10:03возможно,
00:10:04вы смотрите на этот набор требований,
00:10:07и некоторые из вас думают: какое это имеет отношение к сборщику?
00:10:12И на это я бы сказал,
00:10:13конечно,
00:10:14моя команда менеджеров в зале,
00:10:17так что нам не нужно об этом говорить.
00:10:20Но на самом деле,
00:10:21я предполагаю,
00:10:21многие из вас пришли к гораздо более очевидному выводу.
00:10:24Это очень похоже на сигналы.
00:10:28И да, я описываю систему, которая звучит как сигналы.
00:10:31Это способ компоновать вычисления,
00:10:33отслеживать зависимости с некоторой степенью автоматической мемоизации.
00:10:37И я должен отметить,
00:10:38что мы черпали вдохновение из всевозможных систем,
00:10:41особенно из компилятора Rust и системы под названием Salsa.
00:10:45И существует даже академическая литература по этим концепциям,
00:10:49называемым Adaptons,
00:10:50если вам интересно.
00:10:51Хорошо,
00:10:51давайте посмотрим,
00:10:52что,
00:10:53давайте посмотрим,
00:10:54как это выглядит на практике,
00:10:56а затем мы сделаем очень резкий скачок от примеров кода на JavaScript к Rust.
00:11:01Итак, вот пример инфраструктуры, которую мы построили.
00:11:05Функция TurboTask — это кэшируемая единица работы в нашем компиляторе.
00:11:12Итак,
00:11:13мы можем,
00:11:13как только вы аннотируете такую функцию,
00:11:16отслеживать ее,
00:11:17мы можем построить ключ кэша из ее параметров,
00:11:20и это позволяет нам как кэшировать ее,
00:11:23так и повторно выполнять ее,
00:11:25когда нам это нужно.
00:11:28Эти типы VC здесь,
00:11:29вы можете думать о них как о сигналах,
00:11:32это реактивное значение,
00:11:33VC означает «ячейка значения»,
00:11:35но «сигнал» может быть немного лучшим названием.
00:11:39Когда вы объявляете параметр таким образом,
00:11:41вы говорите,
00:11:42что это может измениться,
00:11:44я хочу повторно выполнить его,
00:11:46когда он изменится.
00:11:47И как мы это узнаем?
00:11:49Итак, мы читаем эти значения через `await`.
00:11:52Как только вы ожидаете реактивное значение таким образом,
00:11:55мы автоматически отслеживаем зависимость.
00:11:58И затем,
00:11:58наконец,
00:11:59конечно,
00:12:00мы выполняем фактическое вычисление,
00:12:03которое хотели сделать,
00:12:05и сохраняем его в ячейке.
00:12:07Итак,
00:12:07поскольку мы автоматически отслеживали зависимости,
00:12:11мы знаем,
00:12:11что эта функция зависит как от содержимого файла,
00:12:15так и от значения конфигурации.
00:12:17И каждый раз,
00:12:18когда мы сохраняем новый результат в ячейку,
00:12:21мы можем сравнить его с предыдущим,
00:12:23и затем,
00:12:23если он изменился,
00:12:24мы можем распространять уведомления всем,
00:12:27кто прочитал это значение.
00:12:29Итак,
00:12:29эта концепция изменения является ключевой в нашем подходе к инкрементальности.
00:12:33И да, опять же, самый простой случай прямо здесь.
00:12:37Если файл изменится,
00:12:39Turbo Pack это заметит,
00:12:40аннулирует выполнение этой функции и немедленно перевыполнит ее.
00:12:45И затем,
00:12:45если мы случайно сгенерируем тот же AST,
00:12:48мы просто остановимся на этом,
00:12:50потому что мы вычисляем ту же ячейку.
00:12:53Теперь,
00:12:54что касается парсинга файла,
00:12:55едва ли можно внести в него какое-либо изменение,
00:12:58которое на самом деле не изменит AST.
00:13:00Но мы можем использовать фундаментальную компонуемость функций Turbo Pack,
00:13:06чтобы пойти дальше.
00:13:07Итак,
00:13:08здесь мы видим еще одну функцию кэша Turbo Pack,
00:13:12извлекающую импорты из модуля.
00:13:15Вы можете представить,
00:13:17что это очень распространенная задача,
00:13:18которую мы выполняем в сборщике.
00:13:20Нам нужно извлекать импорты просто для того,
00:13:22чтобы найти все модули в вашем приложении.
00:13:25Мы используем их,
00:13:26чтобы выбрать лучший способ группировки модулей в чанки.
00:13:29И,
00:13:29конечно,
00:13:30граф импортов важен для таких базовых задач,
00:13:33как tree shaking.
00:13:34И поскольку существует так много различных потребителей данных импортов,
00:13:39кэш имеет большой смысл.
00:13:41Так что эта реализация на самом деле не особенная.
00:13:44Это то, что вы найдете в любом сборщике.
00:13:46Мы обходим AST,
00:13:47собираем импорты в какую-то специальную структуру данных,
00:13:51которая нам нравится,
00:13:53а затем возвращаем их.
00:13:55Но ключевая идея здесь в том,
00:13:56что мы сохраняем их в другую ячейку.
00:13:58Так что,
00:13:58если модуль изменится,
00:14:00нам действительно нужно перезапустить эту функцию,
00:14:03потому что мы ее прочитали.
00:14:05Но если подумать о тех изменениях,
00:14:07которые вы вносите в модули,
00:14:09очень немногие из них на самом деле влияют на импорты.
00:14:12Итак,
00:14:12вы меняете модуль,
00:14:14обновляете тело функции,
00:14:16строковый литерал,
00:14:18любую деталь реализации.
00:14:20Это аннулирует эту функцию,
00:14:22и затем мы вычислим тот же набор импортов.
00:14:25И затем мы не аннулируем ничего, что прочитало это.
00:14:29Так что,
00:14:29если вы подумаете об этом в сессии HMR,
00:14:32это означает,
00:14:32что нам действительно нужно перепарсить ваш файл,
00:14:35но нам на самом деле больше не нужно думать о том,
00:14:38как принимать решения о чанках.
00:14:40Нам не нужно думать о каких-либо результатах tree shaking,
00:14:43потому что мы знаем,
00:14:44что они не изменились.
00:14:45Так что мы можем немедленно перейти от парсинга файла,
00:14:48выполнения этого простого анализа,
00:14:50а затем сразу перейти к генерации выходных данных.
00:14:53И это один из способов,
00:14:54благодаря которому у нас очень быстрое время обновления.
00:14:57Так что это довольно императивно.
00:15:02Другой способ осмыслить эту базовую идею — это как граф узлов.
00:15:06Итак, здесь слева вы можете представить холодную сборку.
00:15:12Изначально нам действительно приходится читать каждый файл,
00:15:14парсить их все,
00:15:15анализировать все импорты.
00:15:17И как побочный эффект этого,
00:15:18мы собрали всю информацию о зависимостях из вашего приложения.
00:15:21И затем,
00:15:22когда что-то меняется,
00:15:23мы можем использовать этот граф зависимостей,
00:15:25который мы построили,
00:15:27чтобы распространять инвалидации,
00:15:28возвращаться вверх по стеку и повторно выполнять функции Turbo Pack.
00:15:32И если они производят новое значение,
00:15:34мы останавливаемся там.
00:15:35В противном случае мы продолжаем распространять инвалидацию.
00:15:37Так что отлично.
00:15:41Знаете,
00:15:41это на самом деле своего рода массовое упрощение того,
00:15:44что мы делаем на практике,
00:15:46как вы можете себе представить.
00:15:47Итак,
00:15:48в Turbo Pack сегодня существует около 2500 различных функций Turbo task.
00:15:53И в типичной сборке у нас могут быть буквально миллионы различных задач.
00:15:58Так что это действительно выглядит,
00:16:00возможно,
00:16:00немного больше так.
00:16:01Теперь,
00:16:02я на самом деле не ожидаю,
00:16:03что вы сможете это прочитать.
00:16:04Не смог уместить это на слайде.
00:16:06Так что, возможно, нам стоит уменьшить масштаб.
00:16:08Хорошо, так что это, очевидно, не очень полезно.
00:16:14В действительности у нас есть лучшие способы отслеживать и визуализировать то,
00:16:19что происходит внутри Turbo Pack.
00:16:21Но фундаментально,
00:16:22это работает,
00:16:23отбрасывая подавляющее большинство информации о зависимостях.
00:16:26И теперь я предполагаю,
00:16:27что некоторые из вас,
00:16:29возможно,
00:16:29на самом деле имеют опыт работы с сигналами,
00:16:32возможно,
00:16:33плохой опыт.
00:16:34Знаете,
00:16:35я,
00:16:35например,
00:16:36на самом деле люблю трассировки стека и возможность входить и выходить из функций в отладчике.
00:16:41Так что, возможно, вы подозрительны, что это полная панацея.
00:16:45Например, очевидно, что это сопряжено с компромиссами.
00:16:47И да,
00:16:48на это я бы,
00:16:49конечно,
00:16:50сказал,
00:16:50что,
00:16:51знаете,
00:16:51на самом деле я бы сказал,
00:16:54что вся разработка программного обеспечения — это управление компромиссами.
00:17:01Мы не всегда решаем проблемы точно,
00:17:03но мы действительно выбираем новые наборы компромиссов для предоставления ценности.
00:17:08Итак,
00:17:09чтобы достичь наших проектных целей в отношении инкрементальных сборок в Turbo Pack,
00:17:14мы поставили все наши фишки на эту инкрементальную реактивную модель программирования.
00:17:19И это,
00:17:19конечно,
00:17:20имело некоторые очень естественные последствия.
00:17:23Так что,
00:17:24знаете,
00:17:24возможно,
00:17:25мы на самом деле действительно решили проблему систем кэширования,
00:17:30написанных вручную,
00:17:31и громоздкой логики инвалидации.
00:17:33Взамен нам приходится управлять сложной инфраструктурой кэширования.
00:17:39И,
00:17:39конечно,
00:17:40знаете,
00:17:40для меня это звучит как очень хороший компромисс.
00:17:42Мне нравится сложная инфраструктура кэширования,
00:17:45но всем нам приходится жить с последствиями.
00:17:48Итак,
00:17:48первое,
00:17:49конечно,
00:17:49это просто основные накладные расходы этой системы.
00:17:54Знаете,
00:17:55если подумать,
00:17:56в данной сборке или сессии HMR вы на самом деле не очень много меняете.
00:18:04Так что мы отслеживаем всю информацию о зависимостях между,
00:18:07например,
00:18:07каждым импортом и каждым результатом разрешения в вашем приложении,
00:18:10но вы на самом деле измените лишь несколько из них.
00:18:13Так что большая часть информации о зависимостях,
00:18:15которую мы собираем,
00:18:15на самом деле никогда не нужна.
00:18:16Так что,
00:18:17знаете,
00:18:17чтобы управлять этим,
00:18:19нам пришлось много сосредоточиться на стимулировании,
00:18:21на улучшении производительности этого слоя кэширования,
00:18:24чтобы снизить накладные расходы и позволить нашей системе масштабироваться до все более крупных приложений.
00:18:30И следующее, самое очевидное, это просто память.
00:18:34Знаете,
00:18:35кэши всегда фундаментально являются компромиссом между временем и памятью.
00:18:38И наш в этом смысле ничем не отличается.
00:18:41Наша простая цель состоит в том,
00:18:43чтобы размер кэша масштабировался линейно с размером вашего приложения.
00:18:49Но опять же,
00:18:49мы должны быть осторожны с накладными расходами.
00:18:51Следующий момент немного тонок.
00:18:54Итак,
00:18:55у нас есть множество алгоритмов в сборщике,
00:18:57как вы могли бы ожидать.
00:18:58И некоторые из них требуют понимания чего-то глобального о вашем приложении.
00:19:03Что ж,
00:19:03это проблема,
00:19:04потому что всякий раз,
00:19:05когда вы зависите от глобальной информации,
00:19:07это означает,
00:19:07что любое изменение может аннулировать эту операцию.
00:19:10Поэтому мы должны быть осторожны в том,
00:19:12как мы проектируем эти алгоритмы,
00:19:14тщательно компонуем вещи,
00:19:15чтобы мы могли сохранить инкрементальность.
00:19:17И наконец, это, возможно, немного личная претензия.
00:19:24Все асинхронно в Turbo Pack.
00:19:27И это отлично подходит для горизонтальной масштабируемости,
00:19:29но опять же,
00:19:29это вредит нашим фундаментальным целям,
00:19:31таким как,
00:19:31знаете,
00:19:32отладка и профилирование производительности.
00:19:38Так что я уверен,
00:19:39что многие из вас имеют опыт отладки асинхронного кода,
00:19:42например,
00:19:43в инструментах разработчика Chrome.
00:19:46И это, как правило, довольно приятный опыт.
00:19:48Не всегда идеально.
00:19:49И я уверяю вас, Rust с LLDB отстает на световые годы.
00:19:53Так что для управления этим нам пришлось инвестировать в пользовательские инструменты визуализации,
00:19:59инструментирования и трассировки.
00:20:01И посмотрите на это,
00:20:02еще один инфраструктурный проект,
00:20:04который не является сборщиком.
00:20:07Хорошо, давайте посмотрим, сделали ли мы правильную ставку.
00:20:11Итак,
00:20:11в Vercel у нас есть очень большое производственное приложение.
00:20:17Мы думаем,
00:20:17что это,
00:20:18возможно,
00:20:18одно из крупнейших в мире,
00:20:19но,
00:20:20знаете,
00:20:20мы на самом деле не знаем.
00:20:21Но в нем около 80 000 модулей.
00:20:23Итак, давайте посмотрим, как Turbo Pack справляется с ним.
00:20:26Что касается быстрого обновления,
00:20:28мы действительно доминируем над тем,
00:20:30что может предложить Web Pack.
00:20:32Но это своего рода старые новости.
00:20:33Turbo Pack для разработки уже некоторое время доступен,
00:20:36и я очень надеюсь,
00:20:37что все хотя бы используют его в разработке.
00:20:39Но,
00:20:39знаете,
00:20:40новая вещь здесь сегодня,
00:20:41конечно,
00:20:41в том,
00:20:42что сборки стабильны.
00:20:42Итак, давайте посмотрим на сборку.
00:20:44И здесь вы можете увидеть существенную победу над Web Pack для этого приложения.
00:20:49Эта конкретная сборка на самом деле работает с нашим новым экспериментальным слоем кэширования файловой системы.
00:20:53Так что около 16 из этих 94 секунд — это просто очистка кэша в конце.
00:20:59И это то,
00:20:59над чем мы будем работать,
00:21:00улучшая по мере того,
00:21:01как кэширование файловой системы станет стабильным.
00:21:04Но,
00:21:04конечно,
00:21:05дело в холодных сборках в том,
00:21:06что они холодные,
00:21:07ничего не инкрементально.
00:21:07Итак, давайте посмотрим на фактическую теплую сборку.
00:21:10Итак,
00:21:11используя кэш из холодной сборки,
00:21:13мы можем увидеть это.
00:21:14Так что это просто взгляд на то, где мы находимся сегодня.
00:21:17Поскольку у нас есть эта мелкозернистая система кэширования,
00:21:20мы можем фактически просто записать кэш на диск,
00:21:22а затем при следующей сборке прочитать его обратно,
00:21:24выяснить,
00:21:24что изменилось,
00:21:25и завершить сборку.
00:21:26Хорошо,
00:21:27это выглядит довольно хорошо,
00:21:28но многие из вас думают,
00:21:30что,
00:21:30возможно,
00:21:31у меня лично нет самого большого приложения Next.js в мире.
00:21:34Итак, давайте посмотрим на меньший пример.
00:21:37Веб-сайт react.dev значительно меньше.
00:21:41Это также своего рода интересно,
00:21:42потому что это компилятор React.
00:21:44Неудивительно,
00:21:44что это один из первых,
00:21:46кто внедрил компилятор React.
00:21:47А компилятор React реализован в Babel.
00:21:49И это своего рода проблема для нашего подхода,
00:21:51потому что это означает,
00:21:52что для каждого файла в приложении нам нужно просить Babel обработать его.
00:21:55Так что,
00:21:55и фундаментально,
00:21:57я бы сказал,
00:21:57мы,
00:21:58или я,
00:21:58я не могу сделать компилятор React быстрее.
00:22:01Это не моя работа.
00:22:02Моя работа — Turbo Pack.
00:22:03Но мы можем выяснить, когда именно его вызывать.
00:22:07Итак,
00:22:08глядя на время быстрого обновления,
00:22:10я был на самом деле немного разочарован этим результатом.
00:22:13И оказывается,
00:22:14что около 130 из этих 140 миллисекунд приходится на компилятор React.
00:22:18И Turbo Pack, и Web Pack делают это.
00:22:22Но с Turbo Pack мы можем,
00:22:23после того как компилятор React обработал это изменение,
00:22:27увидеть: о,
00:22:27импорты не изменились.
00:22:29Закинуть это в выходные данные и продолжать.
00:22:31И снова,
00:22:31на холодных сборках мы видим такую последовательную 3-кратную победу.
00:22:37И чтобы было ясно, это на моей машине.
00:22:39Но опять же, никакой инкрементальности в холодной сборке.
00:22:44А в теплой сборке мы видим гораздо лучшее время.
00:22:47Итак,
00:22:47опять же,
00:22:48с теплой сборкой у нас уже есть кэш на диске.
00:22:52Все,
00:22:52что нам нужно сделать,
00:22:53это,
00:22:54по сути,
00:22:54после запуска выяснить,
00:22:55какие файлы в приложении изменились,
00:22:57повторно выполнить эти задачи,
00:22:59а затем повторно использовать все остальное из предыдущей сборки.
00:23:01Итак, основной вопрос: мы уже Turbo?
00:23:05Да.
00:23:06Так что да, это, конечно, обсуждалось в основном докладе.
00:23:09Turbo Pack стабилен начиная с Next 16.
00:23:12И мы даже являемся сборщиком по умолчанию для Next.
00:23:14Так что, знаете, миссия выполнена, пожалуйста.
00:23:17Но. (смех) (аплодисменты)
00:23:23И если вы заметили ту отмену в основном докладе,
00:23:27это я пытался сделать Turbo Pack по умолчанию.
00:23:30Потребовалось всего три попытки.
00:23:31Но то,
00:23:32что я действительно хочу оставить вам,
00:23:34опять же,
00:23:35это вот что.
00:23:35Знаете, потому что мы еще не закончили.
00:23:37Нам еще многое предстоит сделать в плане производительности и завершить работу над слоем кэширования файловой системы.
00:23:42Я предлагаю всем попробовать это в разработке.
00:23:44И это все.
00:23:46Большое спасибо.
00:23:47Пожалуйста, найдите меня, задайте вопросы.
00:23:49(аплодисменты) (бодрая музыка) (бодрая музыка)

Key Takeaway

Turbo Pack использует инновационную инкрементальную реактивную модель с автоматическим кэшированием и отслеживанием зависимостей, чтобы значительно ускорить сборки и горячее обновление, став стандартным сборщиком для Next.js 16, несмотря на некоторые инженерные компромиссы.

Highlights

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

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

Система основана на концепциях «сигналов» и «TurboTask» (кэшируемых единиц работы), которые автоматически отслеживают зависимости и пересчитываются только при изменении входных данных.

Это позволяет значительно сократить время горячего обновления (HMR) и теплой сборки, избегая повторного выполнения несвязанных задач, что демонстрируется на примерах крупных приложений.

Turbo Pack показывает существенное превосходство над Webpack в скорости сборки и горячего обновления для различных размеров приложений, включая крупное производственное приложение Vercel и веб-сайт react.dev.

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

Turbo Pack стабилен, является сборщиком по умолчанию для Next.js 16, и команда продолжает работать над дальнейшей оптимизацией производительности и слоя кэширования файловой системы.

Timeline

Введение и цель Turbo Pack

Люк Сандберг из Vercel представляет себя и тему доклада — Turbo Pack, над которым он работает как инженер-программист. Он делится своим опытом работы в Google над компиляторами и выражает удивление подходом Vercel к веб-инструментам. Основная цель доклада — рассказать о проектных решениях, принятых в Turbo Pack, которые позволяют достигать и поддерживать высокую производительность. Конечная цель Turbo Pack — обеспечить фантастическую производительность, которая будет продолжать развиваться, независимо от сложности проекта.

Основная идея: Инкрементальность и кэширование

Люк объясняет, что холодные сборки важны, но идеальный сценарий — вообще не сталкиваться с ними. Ключевая идея Turbo Pack — инкрементальность через кэширование: кэшировать буквально всё, что делает сборщик. Это означает, что при каждом изменении переделывается только работа, связанная с этим изменением. Стоимость сборки должна масштабироваться по размеру или сложности вашего изменения, а не по размеру или сложности вашего приложения, что гарантирует хорошую производительность для разработчиков.

Наивный сборщик и проблемы кэширования

Докладчик демонстрирует пример «малыша»-сборщика, который парсит каждую точку входа, отслеживает импорты и рекурсивно разрешает ссылки. Он подчеркивает, что такой подход не инкрементален, приводя к многократному парсингу и разрешению одних и тех же файлов, что является «ужасным». Попытка добавить простой кэш для функции парсинга сталкивается с проблемами: как обрабатывать изменения файлов, символические ссылки, разные целевые сборки (клиент/сервер) и мутации AST. Также возникает вопрос о включении сложной конфигурации компилятора в ключ кэша, что делает подход наивным.

Попытки решения проблем кэширования и инвалидации

Люк показывает, как можно улучшить кэширование, включив конфигурацию преобразований в ключ кэша, но это приводит к новым проблемам с надежностью и размером ключей. Для обработки инвалидации добавляется API обратного вызова для чтения файлов, что позволяет удалять устаревшие данные из кэша. Однако это требует передачи колбэков вверх по стеку, чтобы перезапускать сборку при изменении файла, что все еще не является по-настоящему инкрементальным. Такой подход приводит к избыточной работе, такой как повторная генерация всех выходных файлов, и становится почти невозможным для поддержки и добавления новых функций.

Требования к инкрементальному сборщику

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

Решение: Система на основе сигналов (TurboTask и Value Cells)

Люк представляет решение, вдохновленное «сигналами», компилятором Rust и системой Salsa, а также академической литературой по Adaptons. В Turbo Pack это реализовано через `TurboTask` — кэшируемые единицы работы, и `Value Cells` (VC) — реактивные значения. Когда функция `TurboTask` аннотируется, система автоматически отслеживает зависимости через `await` и строит ключ кэша из параметров. При изменении зависимого значения функция перевыполняется, и если новый результат отличается, уведомления распространяются дальше, обеспечивая инкрементальность. Если мы случайно сгенерируем тот же AST, мы просто остановимся на этом.

Пример: Извлечение импортов и оптимизация HMR

Докладчик приводит пример функции `TurboTask` для извлечения импортов из модуля, что является очень распространенной задачей в сборщике. Эта функция читает AST, собирает импорты и сохраняет их в отдельную `Value Cell`. Если модуль изменяется, функция перезапускается, но если набор импортов остается прежним, дальнейшая инвалидация не происходит. Это критически важно для HMR (Hot Module Replacement), так как позволяет пропускать дорогостоящие шаги, такие как принятие решений о чанках или tree shaking. Таким образом, можно немедленно перейти от парсинга файла к генерации выходных данных, что обеспечивает очень быстрое время обновления.

Масштаб и визуализация графа зависимостей

Люк объясняет, что базовая идея инкрементальности может быть осмыслена как граф узлов. При «холодной» сборке приходится читать, парсить и анализировать все файлы, создавая полный граф зависимостей. При последующих изменениях этот граф используется для распространения инвалидаций и повторного выполнения только необходимых `TurboTask` функций. В реальном Turbo Pack существует около 2500 различных функций `TurboTask` и миллионы задач в типичной сборке. Система эффективно отбрасывает подавляющее большинство информации о зависимостях, которая не нужна, что позволяет управлять масштабом.

Компромиссы реактивной модели

Докладчик открыто обсуждает компромиссы, связанные с выбранной инкрементальной реактивной моделью. Вместо ручного кэширования и сложной логики инвалидации, теперь приходится управлять сложной инфраструктурой кэширования, что является «очень хорошим компромиссом». Основные накладные расходы включают отслеживание огромного объема информации о зависимостях, большая часть которой никогда не используется, что требует постоянной оптимизации слоя кэширования. Также возникают проблемы с памятью (классический компромисс время/память), сложность алгоритмов, требующих глобального понимания, и трудности отладки асинхронного кода, что потребовало инвестиций в пользовательские инструменты визуализации и трассировки.

Результаты производительности и бенчмарки

Люк представляет результаты производительности Turbo Pack на реальных приложениях. Для большого производственного приложения Vercel (~80 000 модулей) Turbo Pack значительно превосходит Webpack по скорости горячего обновления. Холодная сборка показывает существенную победу (94 секунды против 300+), а теплая сборка занимает всего 1.5 секунды благодаря кэшу на диске. На примере веб-сайта react.dev (меньшее приложение с компилятором React в Babel) Turbo Pack также показывает 3-кратное ускорение холодной сборки и очень быстрое время теплой сборки (0.5 секунды), оптимизируя вызовы компилятора React.

Заключение и будущее Turbo Pack

Докладчик подтверждает, что «Мы уже Turbo?». Да, Turbo Pack стабилен, начиная с Next.js 16, и является сборщиком по умолчанию для Next.js. Однако работа еще не закончена; предстоит дальнейшая оптимизация производительности и завершение работы над слоем кэширования файловой системы. Люк призывает всех попробовать Turbo Pack в разработке, чтобы оценить его преимущества. Он благодарит аудиторию за внимание и предлагает задавать вопросы, подчеркивая, что команда продолжает работать над улучшением.

Community Posts

View all posts