Острови, які можна перезапустити, на Буні: уроки дизайну з підходу до кераміки
Як фреймворк з орієнтацією на сервер використовує HTML як стандарт, обмежує дію JavaScript до окремих ізольованих блоків, а також керує статичним експортом, помилками, CSP та процесом розгортання.
Більшість сторінок у Інтернеті містять кілька інтерактивних елементів керування, розташованих на їх верхній частині, проте переважна більшість моделей розробки передає всю сторінку браузеру у вигляді JavaScript-додатку. Stoneware – молода фреймворк на TypeScript з відкритим кодом, створена на основі Bun – ґрунтується на протилежній припущенні: HTML – це те, що браузер отримує за замовчуванням, і компонент має явно погодитися на це, перш ніж для нього буде надісланий JavaScript. Вивчення його архітектури є корисним способом зрозуміти концепцію островів, можливість відновлення роботи, статичний експорт, а також менш помітні аспекти розробки фреймворків, такі як повідомлення про помилки, правила безпеки та пакування для розгортання. Наприкінці ви зможете визначити, коли архітектура на основі островів та серверного підходу підходить вашому проекту, та які перешкоди варто остерігатися під час її створення чи впровадження.
Чому сторінка з контентом не повинна перетворюватися на додаток
Архітектура ділить сторінку на два типи територій. Більша її частина — це HTML, який генерується на сервері. Інтерактивні елементи стають „островами“ — невеликими самодостатніми областями, які містять власний JavaScript. Обидва типи елементів опиняються в одному документі у браузері.
Web page
│
┌───────────┴───────────┐
│ │
HTML Islands
│ │
Server rendered JavaScript
│ │
└───────────┬───────────┘
│
Browser
Ключовою наслідком є те, що інтерактивність більше не змушує вас розповсюджувати всю програму. Щоб зробити один віджет інтерактивним, потрібно лише код цього віджета, а не коду всієї сторінки.
Використання Bun замість складання інструментального комплексу
Вибір Bun не означає, що Node.js застарів. Node має величезну екосистему та використовується у багатьох продакшн-проектах з JavaScript, і залишається чудовим середовищем виконання. Цікаво те, що змінюється, коли фреймворк розробляється навколо середовища виконання, яке вже містить інструменти, необхідні більшості проектів.
Bun постачає середовище виконання JavaScript разом із менеджером пакетів, збирачем коду та виконувачем тестів. Для розробників фреймворків це дозволяє уникнути багатьох зайвих елементів. Замість того, щоб шар за шаром додавати фреймворк поверх Node, потім менеджер пакетів, потім окремий збирач коду та окремий виконувач тестів, така архітектура дозволяє розглядати все це як єдину цілісну основу.
Bun
│
┌────────────┼────────────┐
│ │ │
Runtime Tooling Testing
│ │ │
└────────────┼────────────┘
↓
Stoneware
Варто чітко розуміти їхні функції: Bun — це платформа, а Stoneware — фреймворк, який працює на ній. Якщо ви хочете більш детальне порівняння самих середовищ виконання, перегляньте порівняння Node.js, Deno та Bun.
HTML на першому місці, сприймане серйозно
Серверна обробка вмісту існує вже давно, тому фраза «обробити HTML на сервері» здається чимось звичайним. Сильнішою ідеєю, яка лежить в основі Stoneware, є те, що сервер має створювати справді корисний HTML ще до того, як браузер зможе щось зрозуміти про додаток.
Візьмемо компонент, який відображає продукт із назвою та ціною.
<ProductCard
title="MacBook Pro"
price={1999}
/>
Для цього компонента зовсім не потрібно, щоб браузер зберігав його модель у форматі JavaScript. Сервер може перетворити його на звичайний маркап:
<div class="product-card">
<h2>MacBook Pro</h2>
<span>$1999</span>
</div>
Цей вихідний код вже є повним. Його можна читати, пошукові роботи можуть індексувати його, а браузер може негайно відобразити його. Для передачі самого контенту не потрібні жодні скрипти.
Чітке рішення щодо використання JavaScript
Тепер додамо кнопку кошика. На відміну від картки продукту, вона має реагувати на кліки, оновлювати стан та, ймовірно, взаємодіяти з API.
<AddToCart product={product} />
Саме в цьому компоненті обґрунтовується поведінка з боку клієнта, тому він стає окремою одиницею. Решта сторінки залишається у форматі HTML, і кінцевий вигляд сторінки буде таким:
Product page
│
├── Product title HTML
├── Product description HTML
├── Product image HTML
├── Product specifications HTML
│
└── Add to cart JavaScript island
Сторінка не є „без JavaScript“. Вона містить скрипти вибірково, причому цей вибір робиться для кожного окремого компонента, а не для всієї сторінки. Це має значення, оскільки дозволяє зберегти базову версію простою та робить кожен елемент коду, який використовується, очевидним та свідомо обраним рішенням.
Де проходить межа цієї окремої одиниці
Острів позначає межу між контентом, який відображається на сервері, та поведінкою, яка виконується на клієнті. Сайт документації чітко демонструє цю структуру. Текст статті, заголовки, зразки коду, зображення, посилання, навігація та футер — усе це є контентом. Лише кілька функцій є справді інтерактивними: пошук, змінник тем, кнопки копіювання в буфер обміну для блоків коду та, можливо, розгортний дерево навігації.
Documentation page
│
├── Article ─────────────── SSR
├── Code blocks ─────────── SSR
├── Images ──────────────── SSR
├── Navigation ──────────── SSR
│
├── Search ──────────────── Island
├── Theme switcher ──────── Island
└── Copy button ─────────── Island
Саме такі сторінки, переважно складені з контенту з кількома інтерактивними елементами, є метою створення цієї фреймворк-системи.
Гідратація проти можливості відновлення
Справедливою запереченням є те, що все це — це просто SSR. Частково це так: сервер генерує HTML. Різниця полягає у тому, що відбувається після отримання цього HTML.
При класичній гідратації послідовність приблизно така:
- сервер надсилає HTML;
- браузер завантажує JavaScript програми;
Браузер у кінцевому підсумку перебудовує додаток, вихідний формат якого він вже отримав. З точки зору користувача ця робота є суто зайвою: пікселі вже були на екрані.
Stoneware, натомість, розроблений навколо можливості відновлення роботи окремих „островів“. Сервер надсилає HTML разом із усім станом, необхідним для цих інтерактивних островів, а браузер продовжує роботу з тих місць, де сервер зупинився, замість того щоб знову виконувати сторінку для відновлення цього стану. Мета — запобігти ситуації, коли невеликі інтерактивні області змушують до повної перебудови сторінки.
Інша мета оптимізації
Можливість відновлення роботи переосмислює питання продуктивності. Замість того, щоб питати, як прискорити процес ініціалізації всього контенту, потрібно з’ясувати, скільки роботи браузера можна повністю уникнути. Об’єм даних змінюється з „HTML плюс увесь JavaScript додатку плюс крок ініціалізації“ на „HTML плюс лише код, необхідний для взаємодії, з відновлення зі стану, отриманого на сервері“. Для сайтів із великою кількістю контенту це часто є значно кращим рішенням, ніж будь-яка оптимізація процесу ініціалізації.
Якщо ви працюєте з React, такий самий підхід лежить в основі серверних компонентів; архітектура без використання бандлів під час рендерингу описує цей підхід та надає корисне порівняння.
Підхід „сервер на першому місці“ — це не лише сервер
Жоден із цих аргументів не є запереченням проти клієнтських додатків. Колаборативні редактори, інструменти дизайну, ігри в браузері та складні інтерактивні додатки з даними мають зовсім інші вимоги, причому більшість їхніх компонентів за своєю природою є інтерактивними. Архітектура орієнтована на протилежний край спектру: сторінки, де більша частина контенту природним чином генерується на сервері, а взаємодія є винятком.
Статичний експорт ґрунтується на тій самій ідеї
Якщо HTML є стандартом, природним наступним запитанням є те, навіщо взагалі потрібен сервер для сторінок, які ніколи не змінюються залежно від запиту. Stoneware може експортувати додаток у вигляді статичних файлів та передати їх CDN.
Stoneware application
│
▼
export
│
▼
dist/
│
▼
CDN
Документацію, сторінки маркетингу та каталоги продуктів часто можна підготувати заздалегідь та доставляти безпосередньо з краю мережі. Маршрути, яким дійсно потрібна логіка на рівні кожного запиту, можуть залишатися оброблюваними на сервері. Перевага полягає у тому, що переміщення маршруту між статичним та SSR режимами не вимагає зміни моделі програмування для всього додатку.
Динамічні маршрути потребують чіткого списку шляхів
Статичний експорт ускладнюється, як тільки маршрут має параметр. Маршрут на кшталт /products/[sku] у принципі може відповідати необмеженій кількості URL, тому інструмент експорту не може визначити, які сторінки генерувати. Тому фреймворк просить маршрут перелічити їх:
export function staticPaths() {
return products.map(product => ({
sku: product.sku
}));
}
За допомогою цього списку експортер створює справжній HTML-файл для кожного відомого SKU, наприклад dist/products/laptop-1/index.html. Саме тут дизайн фреймворку виходить за межі простого рендерингу JSX: фреймворк має розуміти, як між собою пов’язані маршрути, дані, крок будування та ціль розгортання. Практичний крайній випадок, який потрібно передбачити, — це ситуація, коли динамічний маршрут зовсім не має методу staticPaths(); у такому разі фреймворк повинен або чітко повідомити про помилку під час експорту, або продовжувати рендерити цей маршрут на сервері, адже його тихе ігнорування є найгіршим варіантом.
Повідомлення про помилки є частиною рендерера
У своїй суті рендерер бере компонент та створює HTML. Складність полягає у величезній кількості дочірніх елементів та атрибутів, з якими він мусить працювати: текстові та числові значення, масиви та null, елементи та вкладені компоненти, сигнали та атрибути, виниклі помилки, асинхронні операції та, що неминуче, значення, які не можна відобразити.
Поширеною помилкою є написання <span>{product}</span> тоді, коли мався на увазі <span>{product.name}</span>. Загальне повідомлення про те, що «неможливо відобразити значення типу об’єкт», майже нічого не говорить про те, де шукати проблему. Stoneware натомість описує саме це проблемне значення:
Cannot render a plain object with keys: id, title, price.
і додає шлях до компонента, який призвів до цього:
in <span>
in <Price>
in <ProductCard>
in <Home>
Перелік ключів об’єкта вказує на властивість, яку ви, ймовірно, мали на увазі, а слід компонента вказує на саме файл, який потрібно відкрити. Фреймворк оцінюється не лише за успішний сценарій роботи, а й за те, наскільки швидко він допомагає подолати проблеми у складних ситуаціях.
Коли мікротест відвертає увагу від справжніх витрат
Спочатку збір цих даних про компоненти означав обгортання багатьох операцій відображення у блоки try/catch. Ізольований мікротест показав, що додаткові витрати є незначними. Однак під час реального відображення сторінки обгортання кожного елемента зробило процес відображення приблизно на 38% дорожчим.
Пояснення цьому знайоме кожному, хто проводить тестування JavaScript: у маленькому, повторюваному тесті оптимізуючий компілятор движка може усунути або відкласти більшу частину роботи, яку ви намагаєтесь виміряти, тож отримане число відображає версію коду після оптимізацій.
Мікротестування може вводити в оману, коли час виконання оптимізує саме те, що й вимірюється.
Рішення було структурним: навантаження від відстеження помилок обмежувалося межами компонентів, а окремі елементи використовували дешевші операції збереження та відновлення для підтримки поточного стану. Тестування типу A/B на реальних відображеннях не показало значущих відмінностей. Ширший урок полягає у тому, що продуктивність інструменту відображення залежить як від уникнення погіршень під час додавання функцій, зручних для розробників, так і від справжньої швидкості, а будь-які твердження щодо продуктивності мають підтверджуватися на репрезентативних навантаженнях.
Стандартні налаштування безпеки та порядок обробки даних
Базовий додаток не повинен вимагати від розробника пам’ятати про кожну засаду безпеки перед запуском. Stoneware постачається зі стандартними налаштуваннями, такими як захист від CSRF та підтримка політики безпеки контенту.
Порядок виконання етапів обробки запиту є навмисним:
- Спочатку виконується захист від CSRF;
- Далі — середовище міжсервісного програмування;
- Потім відбувається зіставлення маршруту та відображення контенту;
- Усе завершується через єдиний вихідний пункт відповіді.
Якби середовище міжсервісного програмування виконувалося до перевірки на CSRF, код користувача міг би опинитися на шляху, який обходить межі безпеки фреймворку, наприклад, шляхом раннього повернення або переписування запиту. Фіксація цього порядку в самому фреймворку, замість простої документації та сподівань на дотримання всіма, — саме той тип правила, яким має керувати фреймворк. Єдиний вихідний пункт також гарантує послідовне застосування заголовків, таких як CSP, до кожної відповіді.
Розширення суворих правил CSP без їх вимкнення
Обмежувальна політика є хорошим стандартним варіантом, але справжні сайти завантажують інструменти аналітики, віджети оплати, API, веб-шрифти та карти. Поширеною проблемою є ситуація, коли розробник натрапляє на заблокований скрипт та повністю вимикає CSP. Кращий підхід дозволяє розширювати окремі директиви, зберігаючи при цьому решту елементів стандартної політики недоторканою:
csp: {
scriptSrc: ["https://www.googletagmanager.com"],
connectSrc: ["https://www.google-analytics.com"],
imgSrc: ["https://www.google-analytics.com"],
}
Принцип полягає у безпечних стандартних налаштуваннях разом із чітким списком дозволених елементів для сторонніх сервісів для кожної окремої директиви. При проектуванні такого API необхідно чітко визначити, чи додаватиме наданий джерело коду елемент до стандартної директиви чи замінить її, та задокументувати це рішення, адже обидва варіанти є можливими, і різниця має наслідки для безпеки.
Найскладніші баги з’являються після процесора відображення
Фреймворк не закінчується на рендерері. Код має пройти весь шлях: вихідний код, компілятор, рендерер, процес збирання, ресурси, розгортання, CDN та нарешті браузер. Деякі з найскладніших проблем виникають ближче до кінця цього ланцюга, далеко від коду, який більшість людей вважає „фреймворком“.
Пакування ресурсів для Vercel
Версія 0.1.8 змінила спосіб розгортання Stoneware на Vercel. Для цієї мети генеровані клієнтські частини тепер вбудовані у серверний пакет у вигляді даних Base64, що робить їх частиною пакету, який розгортається платформою, і вони подаються з адреси /_stoneware/*.
Stoneware build
↓
server bundle
├── server code
├── CSS assets
└── island assets
↓
Vercel
↓
/_stoneware/*
Ця функція є необов’язковою та діє лише для цілі Vercel. При розгортанні в контейнері файли вже знаходяться на диску, тож немає причин тримати ще одну копію кожного фрагмента всередині пакету сервера. Основний висновок полягає у тому, що платформи хостингу відрізняються тим, що вони включають під час розгортання, тому обробка ресурсів часто вимагає стратегій, адаптованих до конкретної мети, а не єдиного універсального рішення.
Тести, які підтверджують ефективність виправлення
У проекті є понад 500 автоматизованих тестів, які охоплюють рендерер, маршрутизатор, системи islands та signals, таблиці стилів та обслуговування ресурсів, експорт статичних файлів, механізми CSRF та CSP, цілі розгортання, повідомлення про помилки, а також ворожий чи незвичайний вхідні дані, такі як спроби проходження через структуру шляхів, бінарні файли та некоректні шляхи до ресурсів.
Сама кількість тестів не є ключовою. Більш корисним критерієм є наступне:
Тест на регресію має доводити, що він би провалився до впровадження виправлення.
Тест, який проходить як до, так і після змін, документує поведінку програми, але не запобігає поверненню багу. Перевірка того, чи провалюється новий тест на старому коді, — це проста практика, яка робить набір тестів значно надійнішим.
Використання агента для програмування без замовлення розробки
Більша частина проекту Stoneware була реалізована з використанням Claude як агента для програмування. Він особливо ефективно справлявся з дослідженням розширюваної бази коду, написанням повторюваних елементів інфраструктури, створенням тестів, аналізом помилок, рефакторингом, документуванням архітектури та перевіркою крайніх випадків.
Однак самостійно він не міг вирішити, якою має бути архітектура. Складні питання стосувалися архітектури:
- Для кожного маршруту чи є SSR або статичний експорт правильним режимом?
staticPaths()?Агент може дослідити ці питання та впровадити обраний варіант рішення, але хтось все одно має перевірити цей дизайн та підтвердити результат.
Ймовірне — це не те саме, що правильне
Агент для програмування може дуже швидко створити переконливу реалізацію, і ця швидкість є цінною. Водночас це означає, що він також може швидко припуститися помилки. Проблема з ресурсами Vercel ілюструє цей цикл. Перше рішення копіювало створені ресурси у public/, що здавалося логічним та пройшло місцеві тести; проте реальне розгортання показало, що припущення було хибним. Друга спроба змінила саму модель розгортання. Тестування крайніх випадків потім виявило ще одну помилку в цій версії.
Цей цикл є типовим для програмної інженерії, а не наслідком невдачі ШІ. Змінюється лише швидкість кожної ітерації, що робить верифікацію від початку до кінця у реальному середовищі цілі ще важливішою, а не меншою.
Чому вибір середовища виконання має значення поза межами швидкості
Зведення суті до твердження „Bun швидший за Node“ не відображає справжньої ідеї, адже чиста швидкість у будь-якому разі не є філософією фреймворку. Ще цікавішим експериментом є створення фреймворку на основі припущення, що з самого початку доступні сучасний середовище виконання та інтегрований набір інструментів. Поєднання Bun середовищем виконання, керуванням пакетами, збиранням коду та тестуванням робить його зручною основою для таких архітектурних досліджень.
Де вписується ця архітектура
Цей підхід є ефективним там, де більша частина сторінки — це контент, а лише частина є інтерактивною:
- Документація: статті та зразки коду генеруються на сервері; пошук, перемикання тем та кнопки копіювання є окремими елементами.
- Е-комерція: деталі продуктів, зображення та контент для SEO генеруються на сервері; кошик та фільтри є окремими елементами.
Де, ймовірно, неправильний інструмент
Якщо ваш продукт фактично є десктопним додатком, який працює в браузері — наприклад, спільним редактором, інструментом для обробки графіки, ігрою, інтерактивною панеллю керування чи будь-яким додатком, у якому майже кожен компонент працює на клієнті, — частка сторінки, яка може залишатися у форматі HTML, є невеликою. У такому разі модель „островів“ додає обмеження без значної економії зусиль, тож архітектура, орієнтована на клієнта, ймовірно, буде кращим вибором. Фреймворк може мати чітку думку, не вдаючись до ілюзії, що він підходить для кожного типу завдань.
Спробувати
На момент написання цього тексту керамічний клейоний керамічний матеріал все ще знаходиться у версії 0.1.x, тому очікуйте недосконалостей, і перевіряйте репозиторій для отримання інформації про поточний статус. Серед запланованих покращень — більше інтеграцій, краща діагностика та попередження під час розробки, більше прикладів та цілей для розгортання, краща документація та більше реальних застосувань. Щоб створити основу для проекту та запустити сервер розробки:
bun create stoneware my-app
cd my-app
bun dev
Хорошим першим експериментом буде щось невелике, але з багатим контентом: блог, сайт документації, каталог продуктів, портфоліо чи бізнес-сайт. Під час створення постійно запитуйте себе, наскільки саме сторінка потребує JavaScript.
Основні висновки
- Вважайте HTML стандартним форматом виведення та робіть використання клієнтського JavaScript опційним для кожного компонента; так сторінка буде мати селективне скриптування, а не бути повноцінним додатком.