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

Сначала кеш

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

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

Например, если бы мы обрабатывали кеширование наивным образом, мы могли бы столкнуться со следующей проблемой: мы начинаем загрузку из кеша и из сети одновременно, и, поскольку кэшированный канал готов первым, мы отображаем его пользователю. Затем пользователь ставит лайк сообщению, но как только сетевой ответ на последний канал возвращается, он перезаписывает это сообщение копией, которая не включает аналогичное действие, которое пользователь применил к кэшированной копии (см. Диаграмму ниже).

Чтобы решить эту проблему, нам нужен был способ применить взаимодействия к кэшированному состоянию, но также сохранить эти взаимодействия, чтобы их можно было воспроизвести позже в новом состоянии с сервера. Если вы когда-либо использовали Git или аналогичные системы управления версиями, эта проблема может показаться вам знакомой. Если мы думаем о кешированном состоянии фида как о ветке, а об ответном фиде сервера как о главном, то, что мы фактически хотим сделать, это выполнить операцию перебазирования, применяя коммиты (лайки, комментарии и т. Д.) Из нашей локальной ветки в голову. мастера.

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

  • При загрузке страницы мы отправляем запрос на новые данные (или ждем, пока они будут отправлены)
  • Создайте поэтапное подмножество состояния Redux
  • Пока запрос / отправка находится на рассмотрении, мы сохраняем все отправленные действия.
  • После разрешения запроса мы применяем действие с новыми данными и любыми действиями, которые были отложены, к поэтапному состоянию.
  • Когда этапное состояние зафиксировано, мы просто заменяем текущее состояние на этапное.

Имея промежуточное состояние, все существующее поведение редуктора можно повторно использовать. Он также сохраняет промежуточное состояние (которое содержит самые последние данные) отдельно от текущего состояния. Кроме того, поскольку постановка реализована с использованием Redux, нам просто нужно отправить действия, чтобы использовать ее!

API

Промежуточный API состоит из двух основных функций: stagingAction & stagingCommit (а также пары других для обработки возвратов и крайних случаев, которые мы здесь не будем рассматривать).

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

stagingCommit фиксирует промежуточное состояние в текущее состояние. Если какие-либо асинхронные действия в промежуточном состоянии ожидают выполнения, он будет ждать перед фиксацией. Это похоже на перебазирование в терминах управления версиями, поскольку мы применяем все наши локальные изменения (из ветки кеша) поверх master (новые данные с сервера), оставляя нашу локальную ветку в актуальном состоянии.

Чтобы включить постановку, мы оборачиваем корневой редуктор усилителем редуктора, который обрабатывает действие stagingCommit и применяет поэтапные действия к новому состоянию. Чтобы использовать все это, нам просто нужно отправить соответствующие действия, и все будет сделано за нас. Например, если мы хотим получить новый канал и применить его к поэтапному состоянию, мы можем сделать что-то подобное следующему:

Использование рендеринга с первым использованием кеша как для сообщений каналов, так и для области историй привело к увеличению времени отображения на 2,5% и 11%, а также повысило удобство работы пользователя в соответствии с тем, что доступно во встроенных приложениях Instagram для iOS и Android.

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

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