Как мы web vitals оптимизировали

Рома Ахмадуллин

Дром

Как мы web vitals оптимизировали

Рома Ахмадуллин

Дока — документация про веб

Рома Ахмадуллин

Web Vitals

Web Vitals

Web Vitals

Web Vitals

Web Vitals

Web Vitals

Web Vitals

Web Vitals: что это и как мы их собираем?
CodeFest 15

Web Vitals: что это, как собирать?

CodeFest 16

Web Vitals: как оптимизировать?

4 млн
человек в день
пользуются нашими продуктами

Отправная точка

TTFB
1210 ms
цель < 800 ms
средне
FCP
1690 ms
цель < 1800 ms
хорошо
LCP
2250 ms
цель < 2500 ms
хорошо
CLS
0.05
цель < 0.1
хорошо
INP
467 ms
цель < 200 ms
средне
TTFB
1210 ms
цель < 800 ms
средне
FCP
1690 ms
цель < 1800 ms
хорошо
LCP
2250 ms
цель < 2500 ms
хорошо
CLS
0.05
цель < 0.1
хорошо
INP
467 ms
цель < 200 ms
средне
Цель

Web Vitals в зелёной зоне
в мобильном вебе на 75 перцентиле

Оптимизация TTFB

Поиск проблемных мест

Поиск проблемных мест на сервере

Что мы используем для профилирования?

Что мы используем для мониторинга?

Оптимизация TTFB

Оптимизация запросов к БД

Уменьшение запросов к БД

Поиск дублей запросов к БД

Оптимизация TTFB

Асинхронные запросы

Асинхронные запросы

Оптимизация TTFB

Хэширование ссылок на фотографии

https://cdn.drom.ru/photo/v2/100000006/gen115.jpg
https://cdn.drom.ru/photo/v2/100000006/gen115.jpg
https://cdn.drom.ru/photo/v2/100000006/gen115.jpg
https://cdn.drom.ru/photo/v2/100000007/gen115.jpg
https://cdn.drom.ru/photo/v2/100000006/gen115.jpg
https://cdn.drom.ru/photo/v2/100000007/gen115.jpg
https://cdn.drom.ru/photo/v2/100000008/gen115.jpg
https://cdn.drom.ru/photo/v2/100000006/gen115.jpg
https://cdn.drom.ru/photo/v2/100000007/gen115.jpg
https://cdn.drom.ru/photo/v2/100000008/gen115.jpg
https://cdn.drom.ru/photo/v2/100000009/gen115.jpg
https://cdn.drom.ru/photo/v2/100000006/gen115.jpg
https://cdn.drom.ru/photo/v2/100000006/gen115.jpg
https://cdn.drom.ru/photo/v2/ty0E2fCOT/gen115.jpg

Построение хэша требует времени

Хэширование ссылки на фотографию

Оптимизация TTFB

Перенос получения данных на клиент

Мощно!

🤔 Что ещё можем перенести
на клиент?

Перенесли получение
счётчика подписок на фронтенд

Почему TTFB не уменьшился?

Оптимизируйте в первую очередь широкоиспользуемые фичи.

Оптимизация TTFB

Оптимизация передачи данных с бэкенда на фронтенд

        
            // MODULE_DATA
            {
                "header": {
                    // данные для шапки
                },
                "footer": {
                    // данные для футера
                },
                // остальные данные для страницы
            }
        
        
    
        <script 
            type="text/javascript"
            src="module-a.js"
        >
        </script>
    
    
    
        <script 
            type="text/javascript"
            src="module-a.js"
            data-module="module-a"
            data-module-data="<MODULE_DATA>"
        >
        </script>
    
    
    
        <script 
            type="text/javascript"
            src="module-a.js"
            data-module="module-a"
            data-module-data="<MODULE_DATA>"
        >
        </script>
    
    
    
        <script 
            type="text/javascript"
            src="module-a.js"
            data-module="module-a"
            data-module-data="<MODULE_DATA>"
        >
        </script>
    
    
    
        <script 
            type="text/javascript"
            src="module-a.js"
            data-module="module-a"
            data-module-data="<MODULE_DATA>"
        >
        </script>
    
    
    
        <script 
            type="text/javascript"
            src="module-a.js"
        >
        </script>
        <script 
            type="application/json"
            data-module="module-a"
        >
            <MODULE_DATA>
        </script>
    
    
    
        <script 
            type="text/javascript"
            src="module-a.js"
        >
        </script>
        <script 
            type="application/json"
            data-module="module-a"
        >
            <MODULE_DATA>
        </script>
    
    
    
        <script 
            type="text/javascript"
            src="module-a.js"
        >
        </script>
        <script 
            type="application/json"
            data-module="module-a"
        >
            <MODULE_DATA>
        </script>
    
    

Оптимизация TTFB

Обновление железа

Обновление железа поискового движка

Обновление железа SSR

Оптимизация TTFB

Обновление зависимостей

Обновление PHP

Обновление Node.js

Оптимизация LCP

Оптимизация LCP

Прелоад изображений

        
            <link 
                href="x1.jpg"
                rel="preload"
                as="image"
                imagesrcset="x1.jpg 1x, x2.jpg 2x"
                fetchpriority="high"
            >
        
        

Оптимизация LCP

CDN

CDN

Оптимизация INP

Размер DOM-дерева

Оптимизация INP

Уменьшение DOM-size

Нативный lazy-лоадинг картинок

        
        <img 
            data-lazy="true"
            data-srcset="x1.jpg 1x, x2.jpg 2x"
            data-src="x1.jpg"
        />
        
        
        
        <img 
            data-lazy="true"
            data-srcset="x1.jpg 1x, x2.jpg 2x"
            data-src="x1.jpg"
        />
        
        
        
        <img 
            data-lazy="true"
            data-srcset="x1.jpg 1x, x2.jpg 2x"
            data-src="x1.jpg"
            srcset="x1.jpg 1x, x2.jpg 2x"
            src="x1.jpg"
        />
        
        
        
        <img 
            data-lazy="true"
            data-srcset="x1.jpg 1x, x2.jpg 2x"
            data-src="x1.jpg"
            srcset="x1.jpg 1x, x2.jpg 2x"
            src="x1.jpg"
        />
        <noscript>
            <img 
                srcset="x1.jpg 1x, x2.jpg 2x"
                src="x1.jpg"
            />
        </noscript>
        
        
        
        <img 
            data-lazy="true"
            data-srcset="x1.jpg 1x, x2.jpg 2x"
            data-src="x1.jpg"
            srcset="x1.jpg 1x, x2.jpg 2x"
            src="x1.jpg"
        />
        <noscript>
            <img 
                srcset="x1.jpg 1x, x2.jpg 2x"
                src="x1.jpg"
            />
        </noscript>
        
        
        
        <img 
            data-lazy="true"
            data-srcset="x1.jpg 1x, x2.jpg 2x"
            data-src="x1.jpg"
            srcset="x1.jpg 1x, x2.jpg 2x"
            src="x1.jpg"
        />
        <noscript>
            <img 
                srcset="x1.jpg 1x, x2.jpg 2x"
                src="x1.jpg"
            />
        </noscript>
        
        

Три элемента вместо одного

loading="lazy"

        
        <img 
            srcset="x1.jpg 1x, x2.jpg 2x"
            src="x1.jpg"
        />
        
        
        
        <img 
            srcset="x1.jpg 1x, x2.jpg 2x"
            src="x1.jpg"
            loading="lazy"
        />
        
        
        
        <img 
            srcset="x1.jpg 1x, x2.jpg 2x"
            src="x1.jpg"
            loading="lazy"
        />
        
        

Нативный lazy-лоадинг

Inline svg ➡️ внешние файлы

        
        <svg>
            ...
        </svg>
        
        
        
        <svg>
            ...
        </svg>
        
        
        
        <img src="icon.svg" />
        
        
        
        <svg>
            ...
        </svg>
        
        
        
        <img src="icon.svg" />
        
        
        
        .icon-button {
            mask-image: url(icon.svg);
            background-color: gray;
        }
        
        

Виртуализация галереи

Галерея фотографий

Галерея фотографий

Галерея фотографий

Галерея фотографий

Оптимизация селекта

Поиск проблемных мест локально

Поиск проблемных мест локально

Оптимизация INP

Оптимизация раскрытия свернутых элементов

Оптимизация INP

Оптимизация расчета среднего цвета на фото

        
            const setAverageColorForImages = (images) => {
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                canvas.width = 1;
                canvas.height = 1;

                for (const img of images) {
                    ctx.drawImage(img, 0, 0, 1, 1);
                    const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
                    img.dataset.avgColor = `rgb(${r},${g},${b})`;
                }
            };
        
        
        
            const setAverageColorForImages = (images) => {
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                canvas.width = 1;
                canvas.height = 1;

                for (const img of images) {
                    ctx.drawImage(img, 0, 0, 1, 1);
                    const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
                    img.dataset.avgColor = `rgb(${r},${g},${b})`;
                }
            };
        
        
        
            const setAverageColorForImages = (images) => {
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                canvas.width = 1;
                canvas.height = 1;

                for (const img of images) {
                    ctx.drawImage(img, 0, 0, 1, 1);
                    const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
                    img.dataset.avgColor = `rgb(${r},${g},${b})`;
                }
            };
        
        
        
            const setAverageColorForImages = (images) => {
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                canvas.width = 1;
                canvas.height = 1;

                for (const img of images) {
                    ctx.drawImage(img, 0, 0, 1, 1);
                    const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
                    img.dataset.avgColor = `rgb(${r},${g},${b})`;
                }
            };
        
        
        
            const setAverageColorForImages = (images) => {
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d', { willReadFrequently: true });
                canvas.width = 1;
                canvas.height = 1;

                for (const img of images) {
                    ctx.drawImage(img, 0, 0, 1, 1);
                    const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
                    img.dataset.avgColor = `rgb(${r},${g},${b})`;
                }
            };
        
        
        
            const reviewsData = getDataFromBackend();

            console.log(reviewsData[0]);

            // photoUrl: 'img.jpg'
            // photoAverageColor: 'rgb(130, 20, 94)'
        
        
        
            const reviewsData = getDataFromBackend();

            console.log(reviewsData[0]);

            // photoUrl: 'img.jpg'
            // photoAverageColor: 'rgb(130, 20, 94)'
        
        
        
            const reviewsData = getDataFromBackend();

            console.log(reviewsData[0]);

            // photoUrl: 'img.jpg'
            // photoAverageColor: 'rgb(130, 20, 94)'
        
        

Поиск проблемных мест удаленно

        
            import { onINP } from 'web-vitals';

            onINP(sendToBackend);
        
        
        
            import { onINPAttribution } from 'web-vitals/attribution';

            onINPAttribution(sendToBackend);
        
        
        
            const sendToBackend = (inpMetricData) => {
                const {
                    value, // значение INP
                    attribution: {
                        interactionType, // 'pointer' | 'keyboard'
                        interactionTarget, // CSS-селектор триггера
                        inputDelay, // задержка обработки
                        processingDuration, // время выполнения обработчиков
                        presentationDelay, // задержка отрисовки
                        loadState, // статус загрузки страницы
                    },
                } = inpMetricData;
                logToBackend(...);
            };
        
        
        
        <a 
            href="/123456/"
            class="link-to-bulletin"
            data-stats-name="link-to-bulletin"
            data-stats-track-click="true"
        />
        <a 
            href="/all/"
            class="link-to-list"
            data-stats-name="link-to-list"
            data-stats-track-click="true"
        />
        <a 
            href="/reviews/"
            class="link-to-reviews"
            data-stats-name="link-to-reviews"
            data-stats-track-click="true"
        />
        
        
        
        <a 
            href="/123456/"
            class="link-to-bulletin"
            data-stats-name="link-to-bulletin"
            data-stats-track-click="true"
        />
        <a 
            href="/all/"
            class="link-to-list"
            data-stats-name="link-to-list"
            data-stats-track-click="true"
        />
        <a 
            href="/reviews/"
            class="link-to-reviews"
            data-stats-name="link-to-reviews"
            data-stats-track-click="true"
        />
        
        

Оптимизация INP

Асинхронная отправка аналитики

Мониторинг и алерты

Alertmanager

        
            - alert: InpExitedGreenZone
                annotations:
                    message: '[RUM] INP вышел из зеленой зоны.'
                    summary: 'INP превысил порог в 200 ms в {{ $labels.app_slug }} на {{ $labels.view_type }}'
                    dashboard: 'ссылка на дашборд'
                    runbook: 'ссылка на ранбук'
                expr: |
                    histogram_quantile(
                        0.75,
                        sum by (le, app_slug, view_type) (
                            rate(
                                frontend_rum_inp_histogram_bucket[1d]
                            )
                        )
                    ) > 200
                for: 3d
        
        

Подытожим

TTFB
1210 ms
цель < 800 ms
средне
FCP
1690 ms
цель < 1800 ms
хорошо
LCP
2250 ms
цель < 2500 ms
хорошо
CLS
0.05
цель < 0.1
хорошо
INP
467 ms
цель < 200 ms
средне
TTFB
787 ms
цель < 800 ms
хорошо
FCP
1119 ms
цель < 1800 ms
хорошо
LCP
1360 ms
цель < 2500 ms
хорошо
CLS
0.05
цель < 0.1
хорошо
INP
90 ms
цель < 200 ms
хорошо
TTFB
1210 ms
787 ms
36%
FCP
1690 ms
1119 ms
33%
LCP
2250 ms
1360 ms
40%
CLS
0.05
0.05
0%
INP
467 ms
90 ms
81%

Неожиданный, но приятный бонус

Рост инженерной культуры 🚀

Рекомендации

Советы для ваших оптимизаций

📚 Что ещё почитать?

Web Vitals

Мониторинг

Всем быстрых сайтов!

И соточку в Лайтхаусе :)

Рома Ахмадуллин

Лид фронтенда, Дром
@roma_akhmadullin
Про фронтенд и не только