Pull to refresh

Comments 32

Важный момент — новый подход позволил использовать часть js-шаблонов и на сервере, и в браузере (для отрисовки подгружаемого ajax-ом контента).
Другой подход — рендерить только на клиенте, а когда нужно на сервере — делать это через какой-нибудь prerender.io
1. Скорость визуализации html-я в браузере полученного напрямую с сервера все равно будет быстрее чем загрузка страницы, которая должна будет потом получить данные для рендеринга, затем сам шаблон и только потом сделать шаблонизацию клиентом.

2. Дополнительно придется держать ноду в продакшене на всех бэкендах с дополнительной библиотекой, которая не очень понятно как справляется с нагрузкой.

Подход описанный в статье позволяет получить стабильное решение с использованием уже существующего стека технологий на бэкенде, при условии что проект уже использует Perl, Python или PHP.
1. Не сильно-то и быстрее. Зато при рендеринге на клиенте хтмл и прочие ресурсы можно намертво закешировать, динамически подгружая лишь собственно данные, без раздутого тэгами и классами хтмл.

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

3. Позвольте уж усомниться в стабильности решения, где из трёх совершенно разных языков идёт обращение к четвёртому языку через три реализации моста между ними. Всё же «взять вебкит, загрузить в него страницу и сдампить полученное дерево» — более железное решение, гарантированно совместимое с любыми серверными языками.
1. Вероятнее всего полученный после рендеринга html кешируется, кроме того не все поисковые боты умеют(или умели) шаблонизацию на клиенте. Большая часть данных страницы все же не html.

2. Вряд ли кто-то выводит в продакшн решение без тестирования и замеров :). Ну и в статье вроде есть про выигрыш до 2-х раз:).

3. Из тех же языков ломиться к 4-му всеравно приходится. И про решение про демона все же было упомянуто.

в остальном решение с демоном тоже работает.
1. HTML с данными сложнее кэшировать — нужно избавляться от динамики, добавляя её уже скриптами после загрузки. Кроме того, всё равно перед показом страницы из кеша придётся делать запрос «а не изменилось и что». Вот для ботов-то как раз и необходима шаблонизация на сервере. Для обычного браузера современные высоко динамичные аяксовые сайты проще рендерить на клиенте, общаясь с сервером через тот же апи, через который к нему обращаются и мобильные приложения.

2. О том и речь, что аргумент этот так себе :-)

3. В случае с prerender.io и тп серверу вообще не надо знать про него, куда уж там «ломиться». Он выступает просто как рендеряще-кеширующий прокси.
1. Так я и имел ввиду что шаблонизация на сервере нужна для ботов. При правильно настроенном кэшировании доп. запросы могут вообще не понадобится, кеширование с данными конечно зло, но оно хорошо помогает разгрузить ресурсы. А без рендера на клиенте все равно не обойтись (большАя часть данные прилетает потом).

2. Ну выигрыш в 2 раза по ресурсам сервера я бы не назвал маленьким.

3. Дополнительный «демон» с доп. ресурсами, живущий так же своей жизнью. в случае либы, все-таки все собирается в «один процесс».
2. Речь о том, что вы вменяете в качестве недостатка этому способу то, что вы не замерили скорость его работы. Сомнительный аргумент.

3. Отдельный демон лучше масштабируется. Вы не любите микросервисы?
2. замеры конечно делали, не полноценные, а по «минимальным» и «максимальным» шаблонам.

3. беки тоже отлично масштабируются. Микросервисы дело неплохое, но сложно назвать микросервисом довольно «толстых» демонов отвечающих только за шаблонизацию.
Тема client-side vs server-side rendering довольно холиварная, но все же отвечу :)

1. Здесь все не так однозначно.
Во-первых, данный показатель сильно зависит от того как часто одни и те же юзеры заходят на проект, сколько делают внутренних переходов, от того как часто проекты релизятся и т.п.
У нас в контент проектах человек может зайти, например, в пятницу на tv.mail.ru, посмотреть главную страницу со своими любимыми каналами и уйти на неделю не сделав ни одного дополнительного хита. В середине следующей недели мы выкатываем релиз, в котором очень вероятно изменился какой-то компонент, из-за чего пришлось пересобирать шаблоны и js-библиотеки. В итоге клиент при заходе на тот же ресурс через неделю опять получает всю пачку статики.

Во-вторых, кроме проблемы с «первым запросом» еще есть проблемы с мобильными версиями, где железо зачастую менее производительное чем на десктопах и на шаблонизацию у них будет уходить больше времени. Хотя здесь ситуация постепенно и меняется в лучшую сторону, но людей со старыми смартфонами еще много.

Размер json данных и готового html я бы не стал учитывать вообще, т.к. после gzip они будут иметь сопоставимый вес. Например, если взять главную одного из наших проектов, то на ней html весит 300КБ, а данные для нее 80КБ. После gzip — 45 и 17 соответственно. Да, разница есть, но несущественная.

2. Мы держим v8 в продакшене, который работает в контексте привычного для нас языка и используем его только для шаблонизации. С нагрузкой справляется хорошо, иначе бы не было этой статьи :)
Именно для того, чтобы снять лишние вопросы мы решили выложить все свои наработки по этой теме в паблик. По исходникам должно быть видно, что решение очень простое и все во что тут можно упереться — это сам v8, а точнее его gc, но с этим вы столкнетесь и на ноде, только в еще большем объеме.

3. Реализаций сишных «прослоек» великое множество на любых языках. Они пишутся для того чтобы ускорить какие-то части кода, если стандартных средств языка не хватает. Либо если требуется сделать биндинг к какой-то популярной C/C++ библиотеке. Считайте что это как раз наш случай. Сама прослойка очень тонкая и является, по-сути, интерфейсом к v8monoctx.so. Ее реализация на Perl, Python и PHP весьма простая и она не привносит практически никакого оверхэда в работу проекта.
Сам модуль v8monoctx тоже простой. Его основная задача состоит в том, чтобы создать контекст v8, загрузить в него один раз все js-тулзы и дальше заниматься только компиляцией/кэшированием и выполнением самих шаблонов.

А вот в «железности» Вашего решения есть несколько сомнений:
— речь идет не просто о «взять вебкит, загрузить в него страницу и сдампить полученное дерево», а о работе ноды + вебкита + prerender.io. Это заведомо более длинная цепочка технологий, чем просто один голый вебкит. В нашем случае есть только v8.

— prerender.io на каждый запрос к нему сгенерит кучу внутренних запросов и будет вести себя как браузер, т.е. кэшировать (очевидно на диск) полученные ресурсы, а при повторных запросах валидировать кэш и делать прочую браузерную работу.
Таким образом мы получаем у себя на серверах кучу браузеров, которые кроме шаблонизации делают еще множество вспомогательной работы требующей ресурсы.

— ну, и, пожалуй, самый главный момент. Node.js — это в первую очередь асинхронный сервер. Поэтому его основная задача состоит в том, чтобы обслуживать кучу соединений, а не исполнять код, который требует значительных ресурсов CPU. Либо надо делать какой-то prefork сервер из нее, что уже точно будет выглядеть как зоопарк.

Как уже было сказано в статье наши шаблоны отрабатывают в среднем за 20мс, плюс к этому могут случаться всплески из-за работы gc.
Поэтому предлагая ноду в качестве сервера надо понимать, что пока идет шаблонизация или gc при обработке какого-либо запроса, все остальные клиенты будут ждать свою очередь! Именно поэтому мы выполняем v8 в синхронном режиме в наших обычных воркерах. Они просто предназначены для задач, в которых активно используется CPU.
Если проводить аналогию, то тут напрашивается nginx. Если начать при помощи него ресайзить картинки, писать кучу бизнес-логики на LUA, вкомпилить в него perl и т.п., то nginx превратится в тыкву и вместо обслуживания тысяч запросов будет ждать когда освободится процессор.

Конечно, можно представить, что запросов от поисковиков сравнительно мало, но ситуация опять не так однозначна… Яндекс и Гугл частенько приходят со своими краулерами одновременно, плюс к этому всегда есть пара ботов, которые маскируются под поисковик.
А поскольку «браузер» на бэкенде требует больше ресурсов для рендеринга, то нагрузка от поисковиков может оказаться не такой уж и маленькой как кажется.
И самое противное в этой истории, что любой сканер, ab, siege и т.п. может в любой момент прикинуться гуглом и устроить нагрузку, которую довольно сложно прогнозировать.

Мне кажется предложенное Вами решение можно применять в несильно нагруженных проектах где уже используется нода.
Либо в проектах типа Google Analytics, где вообще ничего не надо индексировать и сайт работает как одно большое приложение написанное на js. Там бэкенду достаточно отдавать только json, а вся шаблонизация ложится на плечи клиента
кроме проблемы с «первым запросом» еще есть проблемы с мобильными версиями, где железо зачастую менее производительное чем на десктопах и на шаблонизацию у них будет уходить больше времени.

А есть бенчмарки? Просто по ссылке, которую я чуть ниже кинул, на мобильных клиентах всё точно так же, как на десктопных: шаблонизация действительно идёт дольше, но отрисовка отрендеренного сервером замедляется точно в той же пропорции.
У нас был проект, который шаблонизировался полностью на клиенте. Основные проблемы у нас возникали как раз с мобильными пользователями. Конкретное время, которое тратилось на шаблонизацию тогда я сейчас уже, увы, не скажу…
Проблема в том, что у нас разный html генерится для веб и мобильной версии, поэтому сравнивать было бы некорректно.
Тут ещё многое зависит от того чем вы рендерите. Есть быстрые способы рендеринга, а есть медленные. Например, стандартный паттерн собирать строку из кусочков, а потом вставлять её в дом в современных браузерах медленнее, чем сразу создавать дом узлы (раньше было наоборот). Или вот, например, AngularJS сначала рендерит шаблон в дом, а потом наполняет его данными — это куда медленнее, чем сначала отрендерить всё дерево, а потом его подклеить в дом.
1.1. Вы пробовали application cache? Клиенту можно быстро показать версию приложения недельной давности, тем временем в фоне подгружая актуальную версию. При желании можно заморочиться и сделать горячую замену старой версии на новую сразу после загрузки оной, но проще не париться и принять, что версия клиента может быть слегка устаревшей.

1.2. То есть клиентская шаблонизация вас смущает по причине скорости, а что gzip не бесплатен не смущает? :-) Впрочем, не очень верится, что для отображения главной на маленьком экране смартфона требуется аж 80кб данных. Думаю тут стоило бы оптимизировать объём загружаемых данных, грузить их лениво, например, а не заставлять старый телефон рендерить 300кб хтмл кода.

3.1. Генерировать хтмл для пользователя через prerender.io разумеется гиблая затея ввиду больших задержек. Речь о том, чтобы не рендерить его для пользователя вообще (только для ботов, которые подождут лишних пол секунды в очереди). И о том, чтобы выпилить с сервера всю логику для загрузки и подготовки данных для шаблонов, оставив лишь простое и стройное API, которое никак не зависит от выбранного шаблонизатора и прочей клиентской логики. У архитектуры «сервер-апи + куча клиентов» масса преимуществ перед архитектурой «сервер, реализующий половину клиентской логики + один клиент, частично её дублирующий + неполнофункциональное апи для полноценных приложений»

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

3.3. Вы не правы, микросервисная архитектура лучше масштабируется, что куда важнее как раз для высоко нагруженных проектов.
1.1. Кроме старого приложения могут потребоваться еще и данные из базы недельной давности… Согласовывать все эти кэши разного уровня тоже непростая задача.sdf

1.2. gzip линейный в плане потребления ресурсов и поэтому беспокоит мало, к тому же он действительно мало потребляет. Конечно, если у вас сервер раздает статику терабайтами, то и gzip начнет вносить свой вклад в нагрузку. Тогда можно заюзать nginx.org/ru/docs/http/ngx_http_gzip_static_module.html
80КБ — это веб версия, конечно

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

p.s. Скорость ответа для поисковиков, насколько я помню — это одна из метрик при ранжировании…
1.1. Зачем именно устаревшие данные? Или вы о том, чтобы показать данные из кэша пока грузятся актуальные? Ну так это тоже не сложно, если используется реактивная архитектура приложения.

1.2. При чём тут нагрузка на сервер? Мы же про бабушкофоны :-)

3. Вебкит отдельным процессом крутится же, нода им просто управляет.

4. Вклад этой метрики даже если и есть, то незначителен на фоне остальных. И это хороший вопрос, скорость какой страницы они мерят: по исходной ссылке или преобразованной.
Касаемо aplication cache. Мы стараемся не делать кэширование страниц документа на стороне клиента, т.к. это может приводить к различным негативным эффектам, например вместе со страницей закэшируется реклама или счетчики.
Все это безусловно можно победить, но надо понимать, что проектов много, у всех своя специфика и просто так всех на «реактивную» архитектуру не пересадить.
К тому же тема статьи именно про унификацию бэкенда, а не про архитектуру клиентской части. Про это скорее всего будет отдельная статья :)

Gzip для мобильного телефона важен, т.к. экономит трафик, без него просто нельзя работать. Отличие от js тут в том, что если gzip на телефоне тратит 1мс cpu на inflate, то он и дальше будет тратить 1мс, в отличии от v8 у которого может неожиданно случиться gc.

А вот про вебкит отдельным процессом интересно. Можете показать какой нибудь список работающих процессов (ps ax) где было бы видно ноду и отдельно работающий вебкит? При условии что в несколько потоков к ноде идут запросы от поисковиков, сколько у него там процессов, трэдов и т.п.?
Appcache предназначен для кеширования не готового DOM, а ресурсов приложения (пустая хтмлина без данных, скрипты, стили, интерфейсные картинки и тп). Кроме быстрого старта приложения он позволяет работать ещё и в оффлайне, работая, например, с localStorage в качестве источника данных.

Боюсь, что не могу.
> 1. Скорость визуализации html-я в браузере полученного напрямую с сервера все равно будет быстрее чем загрузка страницы, которая должна будет потом получить данные для рендеринга, затем сам шаблон и только потом сделать шаблонизацию клиентом.

http://www.onebigfluke.com/2015/01/experimentally-verified-why-client-side.html

If you care about first paint time, server-side rendering wins. If your app needs all of the data on the page before it can do anything, client-side rendering wins.
Да, нас заботит время первой отрисовки, также как и все последующие )
Не первой отрисовки, а начала отрисовки. Server-side раньше начинает, но позже заканчивает.
Это больше синтетический тест. В реальности кроме котиков еще есть вагон js-библиотек, которые используются при рендеринге шаблона. Их тоже надо притащить клиенту, скомпилировать и т.п.
В нашем случае это все живет внутри воркера и грузится в него один раз.
Кроме этого шаблонизация на сервере более предсказуема в плане нагрузки, не говоря уже о мониторинге и прочих метриках за которыми становится легко следить.
Хороший повод выкинуть вагон js-библиотек и воспользоваться чем-то легковесным. Я вот, разрабатываю микромодульный фреймворк, где чтобы отрендерить страничку не нужно грузить кучу огромных либ, а только несколько килобайт кода. Вот небольшое демо, если интересно :-)
Перечисление юзерагентов, кому нужно показывать серверную версию, выглядит очень костыльно. Насколько полный этот список? Как тестировать эту версию, чтобы убедится, что prerender все делает правильно?
Все современные поисковики поддерживают _escaped_fragment_, так что достаточно при наличии этого параметра заворачивать запрос на prerender.io
Косвенно сравнивая метрики, мы получили выигрыш до 2 раз.

И всетаки можете поделится временем синтетических тестов? На сколько я понял, вы замеры проводили для TT и Fest/V8MonoContext.
На синтетике TT проигрывает даже сильнее, но в реальности у v8 есть gc, который слегка снижает разрыв.

Шаблон на TT
[%- FOREACH i IN data; i; END -%]


Исходный шаблон на FEST
<fest:template xmlns:fest="http://fest.mail.ru" context_name="json">
    <fest:get name="common">
                <fest:param name="html">
                        <fest:for iterate="json.data" index="i" value="item">
                                <fest:value>item</fest:value>
                        </fest:for>
                </fest:param>
    </fest:get>
</fest:template>


Тестовый скрипт на перле. Берем массив из тысячи элементов и прогоняем его тысячу раз. Все это работает в виртуальной машине на средней по мощности девелоперской тачке.
use strict;
use warnings;

use JSON::XS;
use V8::MonoContext;
use Template;
use Digest::MD5 qw/md5_hex/;

use Time::HiRes qw/gettimeofday tv_interval/;

my $result_tt = '';
my $result_fest = '';
my $data = {data => [0..999]};
my $fest_file = 'test.xml.js';
my $tt_file = 'test.tpl';

my $v8 = V8::MonoContext->new or die;
my $tt = Template->new(
        ENCODING        => 'UTF-8',
        COMPILE_DIR => '.',
        COMPILE_EXT     => '.ttc',
) or die;

my $t0 = [gettimeofday];
foreach (0..999) {
        my $out = '';
        $tt->process($tt_file, $data, \$out);
        $result_tt .= $out;
}
printf "TT: %f, checksum: %s\n", tv_interval($t0), md5_hex $result_tt;

$t0 = [gettimeofday];
foreach (0..999) {
        my $out = '';
        $v8->ExecuteFile($fest_file, \$out, {json => encode_json($data), append => ';fest["test.xml"]( JSON.parse(__dataFetch()) );'});
        $result_fest .= $out;
}
printf "FEST: %fsec, checksum: %s\n", tv_interval($t0), md5_hex $result_fest;


Результат
TT: 7.066557sec, checksum: 5df3b88b7b9769de8a30398cf847cb93
FEST: 0.357796sec, checksum: 5df3b88b7b9769de8a30398cf847cb93
У верстальщиков, наверно, при виде такого шаблона слёзы радости на глаза наворачиваются :-)
Думаю, скоро сможете спросить у них сами, но уже в рамках другой статьи посвященной фронтенду :)
UFO just landed and posted this here
В рамках одного проекта у нас зоопарка нет. Мы используем классический стек технологий, из необычного только V8, но на это есть свои причины, описанные в этой статье. В отделе три языка для бэкенда по историческим причинам: некоторые проекты переписали с perl на python, потому что перловиков труднее найти, другие проекты влились к нам из других отделов компании.
Вообще в компании много проектов и много команд, которые ими занимаются. Каждая команда выбирает те технологии, которые им больше подходят.
На практике встречался с четырьмя причинами в достаточно небольших компаниях (не аутсорсных, а со своими проектами):

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

— приобретения (по разным сценариям, от недружественного слияния компаний до покупок «коробок») готовых проектов, которые переписывать на «любимом» стеке не рационально, но поддерживать и развивать надо

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

— для разных задач лучше подходят разные инструменты, попытки использования одного «универсального» стека часто приводят к различным проблемам, прежде всего к неэффективному использованию ресурсов, как времени разработчиков, так и аппаратных ресурсов
Sign up to leave a comment.