За последние годы instagram.com претерпел множество изменений - мы запустили истории, фильтры, инструменты для создания, уведомления, прямой обмен сообщениями и множество других функций и улучшений. Однако по мере роста продукта одним неприятным побочным эффектом стало снижение производительности в сети. В течение последнего года мы прилагали сознательные усилия, чтобы сосредоточиться на улучшении этого. Эти постоянные усилия привели к общему сокращению времени загрузки страницы фида почти на 50%. В этой серии сообщений в блоге мы расскажем о проделанной нами работе, которая привела к этим улучшениям.

Отправка данных с использованием раннего сброса и прогрессивного HTML

В части 1 мы показали, как использование предварительной загрузки ссылок позволяет нам запускать динамические запросы раньше при загрузке страницы, то есть до того, как скрипт, который будет инициировать запрос, даже загрузится. С учетом сказанного, выдача этих запросов в качестве предварительной загрузки по-прежнему означает, что запрос не начнется до тех пор, пока HTML-страница не начнет рендеринг на клиенте, что означает, что запрос не может начаться, пока не будут выполнены 2 сетевых обхода (плюс сколько времени потребуется для генерации html на сервере). Как мы видим ниже для предварительно загруженного запроса GraphQL, хотя это одна из первых вещей, которые мы предварительно загружаем в заголовок HTML, до фактического начала запроса может пройти значительное время.

Теоретический идеал состоит в том, чтобы предварительно загруженный запрос начинал выполнение, как только запрос страницы попадает на сервер. Но как заставить браузер запрашивать что-то еще до того, как он получит обратно HTML-код с сервера? Ответ состоит в том, чтобы отправить ресурс с сервера в браузер, и хотя может показаться, что HTTP / 2 push является решением здесь, на самом деле существует очень старый (и часто упускаемый из виду) метод для этого, который имеет универсальную поддержку браузера и не имеет инфраструктурных сложностей реализации HTTP / 2 push. Facebook успешно использует это с 2010 года (см. BigPipe), как и другие сайты в различных формах, такие как Ebay, но, похоже, этот метод в значительной степени игнорируется или не используется разработчиками SPA на JavaScript. Он известен под несколькими названиями - ранний сброс, промывка головы, прогрессивный HTML - и работает, комбинируя две вещи:

  • Кодирование фрагментированной передачи HTTP
  • Прогрессивный рендеринг HTML в браузере

Кодирование передачи по частям было добавлено как часть HTTP / 1.1, и, по сути, оно позволяет разбить сетевой ответ HTTP на несколько фрагментов, которые могут быть переданы в браузер. Затем браузер объединяет эти фрагменты вместе по мере их поступления в окончательный завершенный ответ. Хотя это действительно связано с довольно значительными изменениями в том, как страницы отображаются на стороне сервера, большинство языков и фреймворков поддерживают рендеринг фрагментированных ответов (в случае Instagram мы используем Django в наших веб-интерфейсах, поэтому мы используем объект StreamingHttpResponse ). Причина, по которой это полезно, заключается в том, что он позволяет нам передавать содержимое HTML-страницы в браузер по мере завершения каждой части страницы, а не ждать ответа целиком. Это означает, что мы можем почти сразу передать заголовок HTML в браузер (отсюда и термин ранний сброс), поскольку он не требует большой обработки на стороне сервера. Это позволяет браузеру начать загрузку сценариев и таблиц стилей, пока сервер занят генерацией динамических данных в остальной части страницы. Эффект от этого можно увидеть ниже.

Кроме того, мы можем использовать фрагментированное кодирование для отправки данных клиенту по мере его завершения. В случае приложений с рендерингом на стороне сервера это может быть в форме HTML, но мы можем отправлять данные JSON в браузер в случае одностраничных приложений, таких как instagram.com. Чтобы увидеть, как это работает, давайте рассмотрим наивный случай запуска одностраничного приложения. Сначала исходный HTML-код, содержащий код JavaScript, необходимый для отображения страницы, передается в браузер. После того, как этот сценарий проанализирует и выполнит, он затем выполнит запрос XHR, который извлекает начальные данные, необходимые для начальной загрузки страницы.

Этот процесс включает несколько циклов обмена между сервером и клиентом и вводит периоды, когда и сервер, и клиент бездействуют. Вместо того, чтобы заставлять сервер ждать, пока клиент запросит ответ API, более эффективным подходом было бы, чтобы сервер начал работу над генерацией ответа API сразу после того, как был сгенерирован HTML, и отправил его клиенту. Это будет означать, что к тому времени, когда клиент запустит, данные, скорее всего, будут готовы, и вам не придется ждать еще одного обхода. Первым шагом при внесении этого изменения было создание кеша JSON для хранения ответов сервера. Мы реализовали это с помощью небольшого встроенного блока сценария в HTML-странице страницы, который действует как кеш и перечисляет запросы, которые будут добавлены в этот кеш сервером (это показано в упрощенной форме ниже).

После сброса HTML-кода в браузер сервер может сам выполнить запрос API, а когда он завершится, сбросить данные JSON на страницу в виде тега сценария, содержащего данные. Когда этот фрагмент HTML-ответа получен и проанализирован браузером, это приведет к тому, что данные будут вставлены в кеш JSON. Ключевым моментом, который следует отметить при таком подходе, является то, что браузер будет выполнять рендеринг постепенно по мере получения фрагментов ответа (то есть они будут выполнять полные блоки сценария по мере их потоковой передачи). Таким образом, вы потенциально можете генерировать множество данных параллельно на сервере и сбрасывать каждый ответ в отдельный блок сценария, когда он становится готовым для немедленного выполнения на клиенте. Это основная идея системы BigPipe Facebook, в которой несколько независимых страниц загружаются параллельно на сервер и отправляются клиенту в порядке их завершения.

Когда клиентский сценарий готов запросить свои данные, вместо отправки запроса XHR он сначала проверяет кеш JSON. Если ответ присутствует (или ожидает), он либо отвечает немедленно, либо ожидает ожидающего ответа.

Это приводит к изменению поведения загрузки страницы на следующее:

По сравнению с наивным подходом к загрузке сервер и клиент теперь могут выполнять больше работы параллельно, что сокращает периоды простоя, когда сервер и клиент ожидают друг друга. Влияние этого было значительным: у пользователей настольных компьютеров время завершения отображения страниц увеличилось на 14%, а у мобильных пользователей (с более высокими задержками в сети) - на 23% лучше.

Оставайтесь с нами, чтобы увидеть часть 3

В части 3 мы расскажем, как еще больше повысить производительность, применив при рендеринге данных подход кэширования. Если вы хотите узнать больше об этой работе или хотите присоединиться к одной из наших инженерных команд, посетите нашу страницу вакансий, подпишитесь на нас в Facebook или в Twitter.