Ускоряем

 Мобильный Веб

Сергей Перескоков, Яндекс.Еда

2007 - 2019

Рост  Mobile Web

Скорость парсинга JS

Топики:

  • Ускорение первого рендеринга
  • Ускорение момента интерактивности
  • Отзывчивые большие списки

Что значит быстро?

Время Что видит юзер
0-16мс Время на отрисовку кадра
0-100мс Мгновенный отклик
100-300мс Заметная задержка
300-1000мс Выполнение задачи
> 1000мс Теряется фокус внимания

> 10c - Теряете пользователя

Как мерить скорость?

  • Navigation Timing API
  • PerformanceObserver
  • PageSpeed Insights
  • Lighthouse

Метрики

Метрики

  • First Contentful Paint
  • First Meaningful Paint - FMP
  • Speed Index
  • First CPU Idle
  • Time to Interactive - TTI
  • Estimated Input Latency

 

First Meaningful Paint

Первая отрисовка полезного контента для пользователя

Как ускорить FMP

  • Server rendering
  • Critical CSS
  • Загружать только нужный JS
  • Lazy loading картинок
  • Сократить TTFB

TTFB

Уменьшаем TTFB

HTTP ответ в браузер может прилетать по частям

app.get('/', async (req, res) => {

  res.write(`
    <html>
      <head>
        <script src='...'>
        <link rel='stylesheet'>
  `)

  // TTFB here
  // Start loading scripts/fonts/stylesheets


  const data = await heavyRequest(...)


  res.write(`
          <title>...</title>
        </head>
        <body>
          ${dataToHTML(data)}
        </body>
    </html>
  `)

  res.end()

})

Минусы

  • Непонятно что с GZIP
  • Статус всегда 200
  • Нет HTTP-редиректов
  • Не выставить HTTP-only cookie

Чиним GZIP

import zlib, {Gzip} from 'zlib'

app.get('/', async (req, res) => {

  const stream = zlib.createGzip()

  // Отключаем буферизацию для proxy_pass
  res.setHeader('X-Accel-Buffering', 'no') 
  stream.pipe(res)
  stream.write(`
    <html>
        <head>
            ...
  `)

  // Отправляем содержимое всего стрима в HTTP ответ
  stream.flush()
  
  const data = await heavyRequest(...)
  
  stream.write(`
            <title>...</title>
        </head>
        <body>
           ...
        </body>
    </html>
  `)
  stream.flush()
  stream.end()

})

Чиним HTTP-куки

  1. Создаем уникальный токен на запрос
  2. Отправляем токен как HTTP-only куку
  3. Дожидаемся ответа от бэка (PHPSESSID)
  4. Шифруем PHPSESSID c токеном
  5. Зашифрованный PHPSESSID отправляем в браузер
  6. Из браузера посылаем запрос на расшифровку
  7. В ответе на запрос проставляется кука PHPSESSID

Лоадер

Добавляем лоадер

import zlib, {Gzip} from 'zlib'

app.get('/', async (req, res) => {

  const stream = zlib.createGzip()

  stream.pipe(res)
  stream.write(`
    <html>
        <head>
          <script src="..."></script>
          <div id="splash-screen"></div>
  `)

  
   ...
})

(Плохой вариант)

Добавляем лоадер

import zlib, {Gzip} from 'zlib'

app.get('/', async (req, res) => {

  const stream = zlib.createGzip()

  stream.pipe(res)
  stream.write(`
    <html>
        <head>
          <script src="..."></script>
          <script>
            document.write('<body' + '><' + 'div id="splash-screen"><' + '/div>');
          </script>
  `)

  
   ...
})

PROFIT

Time To Interactive

Момент, когда пользователь может что-то делать со страницей

Что влияет на TTI

  • Скорость загрузки скриптов
  • Количество скриптов
  • Скорость парсинга скриптов
  • Скорость выполнения скриптов

JS Tasks

Hydrate

Rendering

Reflow / Repaint

  • elem.offsetLeft, elem.offsetTop, elem.offsetWidth, elem.offsetHeight, elem.offsetParent
  • elem.clientLeft, elem.clientTop, elem.clientWidth, elem.clientHeight
  • elem.getClientRects(), elem.getBoundingClientRect()
  • getComputedStyle
  • range.getClientRects(), range.getBoundingClientRect()
  • mouseEvt.layerX, mouseEvt.layerY, mouseEvt.offsetX, mouseEvt.offsetY
  • inputElem.focus()
  • inputElem.select(), textareaElem.select()
  • ....

Как улучшить TTI

  • Избегайте Reflow/Repaint
  • Уменьшите размер DOM
  • Сократите кол-во выполняемого JS

Большие списки

  • Товарные списки
  • Фиды/Ленты
  • Таблицы

Как ускорить?

Скрывайте то, что не видно

Как ускорить?

  • Virtual Scroll
  • Visibility Observer
render = () => (
  <InView>
  {
    ({inView}) => {
      if (inView) {
        return (
          <Content />
        )
      }

      return (
        <Skeleton />
      )
    }}
  </InView>
)

Q: А если сразу нужно отрисовать много контента?

A: Отрисовываем, замеряем, удаляем лишнее

Выводы

  • Загружайте только то, что нужно

  • Рендерите, что видно

  • Посматривайте периодически в Lighthouse (CI)

  • Изучите вашу аудиторию перед началом оптимизации

  • Не забывайте про прогресс

Как мерить Impact?

thinkwithgoogle.com/feature/testmysite

Вопросы?

Сергей Перескоков, Яндекс.Еда

Made with Slides.com