Рендеринг — это не про сервер
- пятница, 27 марта 2026 г. в 00:00:05
Когда меня просят подсказать, как структурировать веб-сервис, я всегда начинаю с одного и того же: напишите сервер, который в ответ на HTTP-запросы выдаёт текст в формате HTML.
Это наиболее долговечный, финансово оправданный и понятный пользователю способ написать веб-сервис. Большинство веб-сервисов следует выстраивать именно так, если только у вас нет очень веской причины действовать иначе.
Выслушав меня, веб-разработчики часто отвечают: «о, так вам нравится рендеринг на стороне сервера», на что я обычно, поморщившись, парирую — «да, в какой-то мере». Не распыляйтесь по пустякам, если собеседника отделяет от вас десятилетие невежества. Хотя бы они понимают, о чём я.
Но «рендеринг на стороне сервера» — ужасный термин. Он подразумевает, что сервер не просто больше работает, но и выполняет сложную работу — такую, которую лучше оставить экспертам. Ни то, ни другое не соответствует истине. На самом деле, «рендеринг» на стороне сервера можно организовать почти без труда, какой бы язык программирования вы ни предпочитали.
Стоит вам это понять — и Веб откроется вам таким, каким его вижу я: как самый простой, лёгкий и наиболее мощный интерфейс для вычислений, какой только есть на свете.
Всё, что можно вывести в виде текста, также можно оформить как HTML.
Вот пример на Python, расширяющий встроенный в Python HTTP-сервер так, чтобы на любой GET-запрос он отвечал одним и тем же текстом: <h1>Python webpage!</h1>
from http.server import BaseHTTPRequestHandler, HTTPServer WEBPAGE = "<h1>Python webpage!</h1>\n" class HTMLServer(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.end_headers() self.wfile.write(str.encode(WEBPAGE)) webServer = HTTPServer(("localhost", 8080), HTMLServer) print("Server running at http://localhost:8080") webServer.serve_forever()
Это было бы даже проще, если задействовать flask, но здесь я постараюсь проиллюстрировать мой тезис, воспользовавшись простейшим сервером без каких-либо зависимостей. Нет никакой магии, которая превращала бы HTML-текст в HTML-код. Мы даже не установили заголовок Content-Type в значение text/html.
Устанавливать для заголовка Content-Type конкретное значение — хорошая практика, чтобы браузеру не приходилось гадать.
Выполните этот скрипт, а затем попробуйте взаимодействовать с сервером через curl. Увидите — это просто текст.
$ curl localhost:8080 <h1>Python webpage!</h1>
Что, если открыть в браузере http://localhost:8080? Браузер не покажет простой текст, а отобразит этот HTML в каком-то более динамическом виде. Теги <h1> исчезнут, а оставшийся текст останется большим и жирным.
Чтобы сделать шрифт повеселее, а заодно раскрасить текст, как показано в статье “The Best ‘Hello World’ in Web Development,” просто добавьте к строке тег <style>.

from http.server import BaseHTTPRequestHandler, HTTPServer WEBPAGE = """ <style> body { background-color: lightblue; font-family: 'Comic Sans MS', cursive; } </style> <h1>Python webpage!</h1> """ # Серверный код пропущен для большей ясности
Теперь, достучавшись при помощи curl до конечной точки, отобразим ещё один тег style, и браузер отобразит HTML на приятном голубом фоне, а в качестве гарнитуры выберет comic sans.

Вы заметили, что в предыдущем разделе я употребил слово «отобразит» дважды? В обоих случаях я говорил о действиях браузера, а именно, о преобразовании этого текста:
<style> body { background-color: lightblue; font-family: 'Comic Sans MS', cursive; } <h1>Python webpage!</h1>
в веб-страницу.

Даже столь «простая» задача как «отобразить текст заголовка на голубом фоне» — очень многоступенчатая. В главе 3 отличной книги Пачехи и Харрельсона «Web Browser Engineering» даётся базовое введение в эти этапы. Перейдём сразу к той части, где авторы рассказывают об измерении текста:
Напомним, что
bi_times— это шрифт Times 16-м кеглем: почему же по данным font.metrics шрифт на самом деле имеет 19 пикселей в высоту? Что ж, начнём с того, что кегль 16 означает 16 точек, а «точка» определяется как 72-я доля дюйма. А совсем не 16 пикселей, которые на вашем мониторе расположены, пожалуй, с плотностью около 100 на дюйм. Причём, 16 точек соответствуют не размеру отдельных букв, а размеру металлических литер, на которых в своё время вытравливались буквы. Поэтому сами буквы должны быть мельче 16 точек. Фактически, в различных шрифтах 16-го кегля высота букв отличается.
Ладно.
Оказывается, чтобы просто разместить пару букв на странице, требуется такая глубокая математика вёрстки, над какой большинство веб-разработчиков никогда и не задумывались. Всё это можно изучить (для того и написана вышеупомянутая книга), но веб-рендеринг ошеломительно сложен. Только представьте, что вам требуется самостоятельно реализовать кернинг; а в данном случае вы получаете его даром.
Генерация HTML-текста не называется «рендерингом», поскольку рендеринг — по-настоящему большая и сложная проблема, совершенно не такого масштаба, в каком когда-либо приходилось думать автору сайта. О её решении позаботились разработчики браузеров. Весь необходимый для этого софт сегодня есть у каждого в кармане.
Всё, что требуется сделать автору сайта — набрать текст и правильно заключить его в теги. Никакой математики для этого не требуется.
Как же правильно оформить эту концепцию, если не называть её «рендеринг на стороне сервера»? Да, это генерация текста, но, если точнее — выражение данных в виде текста в формате HTML. Эта техника не только повсеместно доступна без каких-либо специализированных инструментов, но и по-своему интересна!
boroughs = [ "The Bronx", "Manhattan", "Brooklyn", "Queens", "Staten Island" ] # При помощи простых операций над строками # можно выразить этот текст в формате HTML LIST = "<li>".join(boroughs) WEBPAGE = "<h1>NYC Boroughs</h1><ul><li>" + LIST + "</ul>"
Обратите внимание: я не добавил сюда закрывающих тегов </li>. На самом деле, в HTML не требуется закрывать элементы списка. Такую вольность обычно не рекомендуется допускать, но, если пойти на неё — оказывается, что элементы списка становится гораздо проще генерировать программно.
Операции над строками — это азбука программирования. Достаточно выучить пару HTML-элементов — и вот вы уже при помощи простейших строковых операций можете интерактивно представить любые задачи, выполняемые в вашем коде. Получающийся текст не очень симпатично выглядит вне браузера, но внутри браузера он делает что надо.
<h1>NYC Boroughs</h1><ul><li>The Bronx<li>Manhattan<li>Brooklyn<li>Queens<li>Staten Island</ul>

Естественно, тот же самый набор данных — воспользовавшись теми же приёмами — можно было бы выразить в виде JSON.
boroughs = [ "The Bronx", "Manhattan", "Brooklyn", "Queens", "Staten Island" ] LIST = '","'.join(boroughs) WEBPAGE = '{ "nyc_boroughs": ["' + LIST + '"] }' # Серверный код опущен для ясности
Правда, таким способом мы не так много добьёмся, поскольку в JSON нет инструментов для управления гипермедиа. Но это, как минимум, возможно — на случай, если вам нужно дёшево и сердито получить вывод JSON, а доступа к библиотеке JSON у вас по какой-то причине нет.
Очевидно, эта задача решаема и при помощи модуля JSON языка Python, но мне время от времени приходилось при программировании моих скриптов делать подобные вещи на bash или awk. Просто помните, что это возможно!
{ "nyc_boroughs": ["The Bronx","Manhattan","Brooklyn","Queens","Staten Island" ] }
Суть не в том, что вам (обязательно) следует генерировать HTML или JSON путём манипуляций над строками, а в том, что обе эти технологии работают на сопоставимом уровне сложности и абстрагирование. Я против термина «рендеринг на стороне сервера», поскольку он подразумевает иное.
Чтобы приступить к «рендерингу на стороне сервера», вам всего лишь нужно представить ваши данные в формате HTML и вернуть их с сервера.
Я предпочитаю термин «HTML API». Разработчики знакомы с JSON API, а HTML API работает ровно так же, только вместо JSON он возвращает HTML. Формулировка «HTML-отклики» тоже нормальная.
(HTML API также являются REST API, но, упомянув «REST API», вы сразу же должны будете отправить коллегам вторую статью, а это уже другой разговор.)
Многие «подвисают» от мысли, что HTML не может быть API (интерфейсом прикладного программирования), поскольку HTML позиционируется как человеко-читаемый язык, а API предназначены для чтения компьютерными программами. Но это не вполне верно.
Рассмотрим список боро (районов) Нью-Йорка в JSON и HTML и сопоставим эти варианты.
<h1>NYC Boroughs</h1> <ul> <li>The Bronx <li>Manhattan <li>Brooklyn <li>Queens <li>Staten Island </ul> { "nyc_boroughs": [ "The Bronx", "Manhattan", "Brooklyn", "Queens", "Staten Island" ] }
Ни то, ни другое на самом деле не предназначено для чтения конечному пользователю. Конечный пользователь должен увидеть отформатированный список!
HTML — это гипермедийный формат. То есть, в нём содержатся структурированные данные и стандартный интерфейс, который может отобразить браузер. В JSON API просто кодируются данные — без представления. При использовании HTML API мы не перемещаем сложность с клиента на сервер; при таком подходе менее полезное представление в формате JSON вообще выводится из оборота.
С такой точки зрения HTML API предназначен для коммуникации софта с софтом. Сервер обращается к пользовательскому браузеру, а не к клиентскому приложению JavaScript. Пользовательский браузер считывает HTML API и отображает полученную информацию в виде графического пользовательского интерфейса (GUI).
При создании реальных веб-сайтов, а не скриптов хочется иметь под рукой более продвинутые инструменты, а не просто объединять строки. Для начала хорошо бы изучить движок-шаблонизатор.
Шаблонизаторы — это библиотеки для генерации структурированного текста. Распространённый шаблонизатор — Jinja. С его помощью можно в рамках обычных потоков управления, например, циклов «for», собирать HTML-строки.
<h1>NYC Boroughs</h1> <ul> {% for borough in boroughs %} <li>{{ borough }} {% endfor %} </ul>
В настоящее время инструмент Jinja является кроссплатформенным, но есть и множество других шаблонизаторов, чей синтаксис воспринимается как нативный для конкретного языка (напр., для rust, zig, ocaml, common lisp). В этом заключается одно из основных преимуществ, возникающих, если отталкиваться от «HTML — это текст». Вы можете далее использовать любой язык программирования, который вам наиболее подходит.
В профессиональной веб-разработке важно использовать шаблоны, так как в них предусмотрены безопасные умолчания для экранирования пользовательского контента. (В сущности, следует экранировать весь контент, динамически вставляемый в HTML-документ). Также в шаблонах существуют приятные допуски для многократного использования кода.
Но одно из самых лучших свойств шаблонов — в том, что их легко понять. Это прямолинейные инструменты для автоматизации текстового вывода, не сохраняющие состояния. Если текст отсутствует или неправильно экранирован, либо поставлен не там, где нужно — такая проблема, как правило, легко поддаётся диагностике и отладке.
В наше время React поддерживает HTML API, но ценой выстраивания чрезвычайно сложной архитектуры. В сущности, React преобразует всю логику пользовательских взаимодействий с сайтом в загружаемый по нарастающей JSON, a затем обратно в HTML. Работа заключается в генерации HTML вместе с некоторым количеством дополнительных промежуточных шагов. Зачем вообще всем этим заниматься, если можно просто сгенерировать HTML?
Для решения этого вопроса команда React предложила React Server Components, которые позволили снять проблему «как выбирать данные в React?», донимавший команду на протяжении всех 2010-х.
Ведь хочется использовать React.
Если вы любите React, и вам нравится с ним работать — ваше право. Просто не путайте сложность React Server Components с собственной сложностью веб-платформы. Если нет требования использовать React, то не сохраняющий состояния гипертекстовый API (также известный как REST API), позволяет добиться сопоставимой (или даже лучшей?) производительности без всей этой сложности.
Сайты не сложные. Приходится преодолеть немало подводных камней, если требуется написать динамический веб-сервис с формой для входа, базами данных, пользовательским контентом (а также для этого нужен высокий профессионализм). Но именно в пределах веб-страницы вы выражаете данные, поступающие с вашего сервера как интерфейс, информация с которого должна быть представлена в пользовательском браузере — и это довольно просто. Эта задача, в самом деле, решается почти без всяких специализированных инструментов.
В конце концов, это просто объединение строк.
Спасибо Меган Денни и Карсону Гроссу за обратную связь по черновикам этой статьи.
Рекомендую почитать многое из написанного Дэном Абрамовым — в частности, в статье «Progressive JSON» уловлено много важных вещей.
В движках‑шаблонизаторах также может предусматриваться статическая проверка типов.
Притом, что мне никогда не понять, что в голове у человека, собирающегося писать веб‑приложения на Prolog — такие люди существуют, и для своей затеи они даже разработали инструментарий.
На мой взгляд, сторонники React слишком рьяно отстаивают идею, что обязательно требуется применять весь арсенал React в тех случаях, где вполне хватило бы и просто элемента <form>. Не хочу сражаться тут с соломенными чучелами, так что просто оговорюсь, что мне эта точка зрения известна, и я её не разделяю.
Все примеры, приведённые в этой статье — это статический текст, но до интерактивности им не хватает лишь тегов <a>, <button> или <script>.
Те, кто изучал веб‑разработку примерно до 2016 года, без всяких проблем улавливают эту концепцию. Я просто говорю «Это как Rails» или «Это как JavaServerPages». (Но, если вы сегодня пишете на Java, то рекомендую вам ознакомиться с Thymeleaf.)