Опытные разработчики знают, что сайт должен быть легким, быстрым и отзывчивым, ведь даже пара дополнительных миллисекунд может стоить потери клиента. Сейчас мы рассмотрим эффективные способы оптимизации фронтенда, которые касаются как загрузки страницы, так и ее поведения во время работы. Вы узнаете о технических деталях работы браузера, научитесь избегать ошибок, замедляющих отрисовку интерфейса, и поймете, когда стоит применять те или иные подходы для достижения плавной и быстрой работы веб-приложения.
1. Сокращайте количество HTTP-запросов
Каждый HTTP-запрос выполняется в несколько этапов. Основные из них:
- Определение IP-адреса через DNS.
- Установка соединения (TCP-рукопожатие).
- Отправка запроса браузером.
- Получение и обработка запроса сервером.
- Отправка ответа сервером.
- Получение ответа браузером.
Возьмем в качестве примера HTTP-запрос на загрузку файла размером 28,4 Кб:

Чтобы разобраться, что происходит в процессе выполнения запроса, важно понимать терминологию:
- Queueing (Ожидание в очереди) — время, когда запрос стоит в очереди и ждет своей отправки.
- Stalled (Задержка) — время между установкой соединения и началом передачи данных. Сюда также входит время на согласование с прокси-сервером.
- Proxy negotiation (Согласование с прокси) — время переговоров с прокси-сервером.
- DNS Lookup (Разрешение DNS) — время, затраченное на поиск IP-адреса домена. Чем больше разных доменов используется на странице, тем больше времени уходит на DNS-запросы.
- Initial Connection / Connecting (Установление соединения) — время на создание соединения с сервером, включая TCP-рукопожатие, повторные попытки соединения и процесс настройки SSL.
- SSL — время, потраченное на установку защищенного соединения (SSL-рукопожатие).
- Request sent (Отправка запроса) — время, необходимое для фактической отправки запроса; как правило, очень короткий промежуток, около миллисекунды.
- Waiting (TFFB, время до первого байта) — промежуток между отправкой запроса и получением первого байта ответа.
- Content Download (Загрузка контента) — время, которое тратится на прием всех данных от сервера.
Из этого примера видно, что фактическое время загрузки данных составило лишь 13,05 мс из общего времени 204,16 мс — то есть только 6,39% всего времени ушло на прием контента, а остальное заняли накладные расходы на соединение и ожидание.
👉 Чем меньше файл — тем ниже процент полезной загрузки по отношению ко времени всего запроса. Чем файл крупнее — тем эффективнее используется соединение. Именно поэтому рекомендуется объединять несколько мелких файлов в один большой, чтобы сократить количество HTTP-запросов и сэкономить время.
ℹ️ Дополнительная информация от разработчиков Google:
2. Используйте HTTP/2
По сравнению с HTTP/1.1, протокол HTTP/2 имеет целый ряд преимуществ, которые позволяют сайтам работать быстрее и эффективнее.
Чем HTTP/2 лучше HTTP/1.1:
1️⃣ Быстрая обработка запросов
В HTTP/1.1 сервер вынужден читать данные по байтам, пока не встретит специальный разделитель CRLF (возврат каретки + перевод строки), чтобы понять, где заканчивается заголовок и начинается тело запроса.
В HTTP/2 все проще — это фреймовый протокол: данные разбиты на фреймы, и у каждого фрейма заранее указана его длина. Сервер сразу знает, сколько данных нужно принять, и не тратит время на поиск разделителей.
2️⃣ Мультиплексирование
В HTTP/1.1 на одном TCP-соединении можно передать только один запрос за раз. Если браузеру нужно одновременно загрузить 10 файлов (например, картинки и стили), он создаст 10 отдельных TCP-соединений.
HTTP/2 умеет передавать много запросов одновременно через одно соединение. Это называется мультиплексирование. Каждому запросу присваивается свой идентификатор потока. Благодаря этому:
- Данные могут приходить в любой последовательности.
- Браузер соберет их обратно в правильном порядке.
- Отпадает нужда открывать множество соединений, экономится время и ресурсы.
3️⃣ Сжатие заголовков
HTTP/2 поддерживает сжатие заголовков. Например, два HTTP-запроса к разным серверам выглядят так:
Первый запрос:
:authority: unpkg.zhimg.com
:method: GET
:path: /za-js-sdk@2.16.0/dist/zap.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
...
user-agent: Mozilla/5.0 ...
Второй запрос:
:authority: zz.bdstatic.com
:method: GET
:path: /linksubmit/push.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
...
user-agent: Mozilla/5.0 ...
Как видно, большинство строк повторяется. Передавать их каждый раз заново — неэффективно. Для оптимизации в HTTP/2 используется механизм таблицы заголовков:
- Когда клиент отправляет заголовки, он сохраняет их в таблицу.
- Сервер сохраняет у себя ту же таблицу.
- При следующих запросах клиент может передавать не сами заголовки, а просто номера из таблицы.
Например, если таблица выглядит так:

То вместо повторной передачи текста клиент просто отправит:
62 63 64
Сервер сам восстановит эти заголовки по номерам. Это сильно экономит трафик и ускоряет обмен данными.
4️⃣ Приоритеты запросов
HTTP/2 позволяет указывать приоритет для разных запросов. Например:
- важно: CSS-файл;
- менее важно: изображение.
5️⃣ Контроль потока
Так как у TCP-соединения пропускная способность ограничена, важно уметь распределять ее между несколькими параллельными запросами. HTTP/2 позволяет умно управлять потоком: если один запрос слишком жадный и забирает много трафика, сервер/клиент может замедлить его, чтобы другие запросы не страдали.
6️⃣ Предзагрузка по инициативе сервера
В HTTP/2 сервер может не только ответить на запрос, но и заранее отправить клиенту дополнительные файлы, которые могут понадобиться позже. Например, когда браузер запрашивает HTML-страницу, сервер, помимо самой страницы, сразу отправит CSS и JavaScript-файлы, которые на ней используются, еще до того, как браузер их запросит. Это снижает задержки и ускоряет загрузку.
ℹ️ Дополнительная информация по HTTP:
3. Используйте серверный рендеринг
Клиентский рендеринг — классическая схема для современных SPA-сайтов:
- Браузер загружает пустой HTML-файл с минимумом содержимого — обычно там только
<div id="app"></div>
. - Потом браузер скачивает JavaScript-файлы.
- JavaScript запускается на стороне клиента, собирает страницу, создает структуру (DOM) и только потом показывает контент.
Минусы клиентского рендеринга:
- Пользователь видит пустой экран, пока все JS-скрипты не загрузятся и не отработают.
- Поисковые роботы могут не дожидаться рендеринга контента.
При серверном рендеринге, напротив, основная работа выполняется на сервере, а не на клиенте:
- Сервер заранее создает готовую HTML-страницу со всем контентом.
- Когда браузер получает такой HTML — он сразу показывает текст и структуру.
- Пока пользователь смотрит страницу, параллельно загружаются JavaScript-файлы, чтобы сделать страницу интерактивной (например, обработать клики и анимации).
Плюсы серверного рендеринга:
- Быстрая загрузка первого экрана. Представим, что сайт состоит из 4 файлов каждый по 1 Мб. При клиентском рендеринге браузеру нужно сначала загрузить эти 4 файла = ~4 МБ, прежде чем пользователь увидит что-то на экране. При серверном рендеринге сервер возвращает уже собранный HTML, обычно это ~400-500 КБ, и пользователь сразу видит текст и структуру.
- Лучшиe SEO-показатели — поисковые боты получают готовую страницу с текстом, а не пустой div.
Минусы серверного рендеринга:
- Более сложная реализация.
- Увеличение нагрузки на сервер.
ℹ️ Дополнительная информация:
4. Используйте CDN для статических ресурсов
Когда пользователь заходит на сайт, скорость загрузки сильно зависит от расстояния до сервера: чем дальше сервер — тем дольше идут запросы, чем ближе сервер — тем быстрее загружаются файлы.
CDN решает эту проблему: статические файлы (изображения, стили CSS, JavaScript и т. д.) копируются на серверы, которые расположены по всему миру. Когда пользователь заходит на сайт — файлы загружаются не с центрального сервера, а с ближайшего к нему.
Как работает запрос без CDN:
- Браузер обращается в DNS, чтобы узнать IP-адрес сайта.
- DNS-резолвер делает несколько запросов (корневой сервер, сервер доменной зоны, авторитетный сервер) и получает IP-адрес сайта.
- Браузер отправляет запрос на этот IP.
- Сервер сайта отвечает и отдает ресурсы.
Как работает запрос с CDN:
- Браузер, как обычно, запрашивает IP-адрес сайта через DNS.
- DNS-резолвер получает IP-адрес не самого сайта, а специальной системы балансировки — GSLB (Global Server Load Balancing).
- GSLB определяет географическое положение пользователя (по IP) и выбирает ближайший SLB-сервер (Server Load Balancing).
- DNS возвращает браузеру адрес этого ближайшего SLB.
- Браузер делает запрос на SLB.
- SLB выбирает оптимальный кэш-сервер и перенаправляет туда браузер.
- Кэш-сервер сразу отдает запрошенные файлы, если они есть в кэше, либо запрашивает оригинальный ресурс с главного сервера, возвращает его браузеру и сохраняет у себя для следующих запросов.
5. Размещайте CSS в <head>, а JavaScript — внизу страницы
Это очень важное правило для быстрой загрузки страниц и правильного отображения контента. Когда браузер загружает страницу, он идет сверху вниз по HTML-коду. Но не все теги обрабатываются одинаково:
- CSS блокирует рендеринг (отображение страницы). Пока стили не загрузятся и не применятся — браузер не будет показывать страницу пользователю, чтобы избежать «мигания» некрасивой страницы без стилей.
- JavaScript блокирует парсинг HTML и построение CSSOM. Когда браузер видит
<script>
безasync
илиdefer
— он останавливает разбор HTML, загружает скрипт, выполняет его, а только потом продолжает парсить страницу.
👉 CSS нужно размещать в <head>
— чтобы стили были готовы до того, как браузер покажет контент. Если этого не сделать — пользователь сначала увидит голую страницу без оформления, а потом она резко «переоденется» (это называется Flash of Unstyled Content, FOUC).
👉 JavaScript нужно размещать внизу, перед </body>
. Так HTML соберется и отобразится максимально быстро, а скрипты подгрузятся уже после, не мешая рендерингу.
👉 Если скрипты обязательно должны быть подключены в <head>,
нужно добавить атрибут defer
. Тогда загрузка будет происходить асинхронно, а выполнение начнется только после полной загрузки и построения DOM:
<script src="your-script.js" defer></script>
ℹ️ Дополнительная информация от разработчиков Google:
6. Используйте иконочные шрифты вместо изображений
В чем плюсы иконок-шрифтов:
- Иконки-шрифты вставляются в HTML так же, как обычный текст. Например:
<i class="fa fa-car"></i>
- И потом их можно легко стилизовать через подключаемый CSS-файл:
.fa-car {
color: red;
font-size: 48px;
margin: 20px;
}
Или прямо в HTML:
<i class="fa fa-car" style="color: blue; font-size: 60px; margin: 10px;"></i>
- Шрифтовые иконки — векторные изображения. Они масштабируются без потери качества на любом разрешении, в отличие от обычных растровых изображений (PNG, JPG).
- Маленький размер файлов. Один шрифт с несколькими иконками обычно весит несколько килобайт, в отличие от набора отдельных картинок, где каждая иконка может занимать десятки или сотни килобайт.
- Чтобы сделать файл со шрифтами еще легче, можно использовать инструменты для удаления ненужных символов и иконок, например fontmin-webpack, который автоматически сжимает шрифты и оставляет только те символы, которые реально используются на сайте.
7. Грамотно используйте кэширование, чтобы избежать повторной загрузки одних и тех же ресурсов
Когда пользователь заходит на сайт, браузер обычно загружает картинки, стили, скрипты и другие файлы с сервера. Чтобы ускорить загрузку, браузеры используют кэш — сохраняют уже загруженные файлы локально и при повторном посещении сайта могут просто взять эти файлы из кэша, не обращаясь к серверу.
Сервер сообщает браузеру, сколько времени можно хранить файлы в кэше, с помощью специальных HTTP-заголовков:
- Expires — указывает точную дату и время, до которого файл можно брать из кэша, не обращаясь к серверу.
- max-age — задает время хранения в секундах, начиная с момента загрузки файла.
👉 Обычно рекомендуется использовать max-age, потому что этот способ проще и гибче в использовании.
Что делать, если файл обновился?
Если вы изменили, например, styles.css, а у пользователя в кэше осталась старая версия — браузер будет ее показывать, даже если файл на сервере уже новый. Чтобы решить эту проблему, нужно обновить ссылку на файл. В сборщиках вроде webpack это делается автоматически:
output: {
filename: '[name].[contenthash].js'
}
8. Сжимайте файлы
Когда вы загружаете сайт, браузер скачивает кучу файлов: JavaScript, CSS, HTML, изображения. Чем больше весят эти файлы, тем дольше пользователь ждeт загрузки страницы. Поэтому важно сжимать файлы перед отправкой.
Если вы используете сборщик вроде webpack, там есть плагины для сжатия разных типов файлов:
- JavaScript — с помощью UglifyPlugin (удаляет пробелы, сокращает имена переменных).
- CSS — с помощью MiniCssExtractPlugin (выделяет CSS в отдельные минифицированные файлы).
- HTML — с помощью HtmlWebpackPlugin (оптимизирует разметку).
Однако даже после сборки и минификации файлы могут быть довольно большими. Тут помогает gzip-сжатие — это алгоритм, который уменьшает размер файла на 40–80%, в зависимости от его содержания. Например, если средний JavaScript-файл после сборки весит 1,4 Мб, то после gzip-сжатия — всего 573 Кб. Это почти в 2,5 раза меньше!
Как включить gzip:
- Браузер отправляет в заголовке Accept-Encoding информацию, что готов принять сжатые данные:
Accept-Encoding: gzip, deflate, br
- Сервер проверяет этот заголовок и, если поддерживает gzip, отправляет файл в сжатом виде, указав:
Content-Encoding: gzip
Браузер автоматически распакует файл, и пользователь этого даже не заметит.
Как настроить gzip-сжатие в проекте (webpack + Node.js):
- Установите нужные плагины:
npm install compression-webpack-plugin --save-dev
npm install compression
- В webpack.config.js добавьте плагин:
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
plugins: [
new CompressionPlugin()
]
}
- В Node.js (например, Express) подключить gzip-сжатие:
const compression = require('compression');
const express = require('express');
const app = express();
// Включить сжатие перед всеми остальными middleware
app.use(compression());
9. Оптимизируйте загрузку изображений
Когда сайт загружает слишком много изображений сразу, это увеличивает время загрузки страницы и ухудшает впечатление пользователя. Поэтому изображения важно оптимизировать. Вот основные способы:
1️⃣ Ленивая загрузка
Идея проста: не загружать картинки сразу, а подгружать их только тогда, когда пользователь доскроллил до места, где они видны.
HTML
Картинка не загружается, пока она не попала в область видимости:
<img data-src="https://site.com/picture.jpg" alt="описание">
JS
Когда скрипт меняет src, браузер начинает загружать изображение:
const img = document.querySelector('img');
img.src = img.dataset.src;
2️⃣ Адаптивные изображения
Браузеры могут автоматически выбирать подходящее изображение для экрана пользователя:
<picture>
<source srcset="large.jpg" media="(min-width: 801px)">
<source srcset="small.jpg" media="(max-width: 800px)">
<img src="small.jpg" alt="описание">
</picture>
Или с помощью CSS:
@media (min-width: 769px) {
.bg {
background-image: url(bg1080.jpg);
}
}
@media (max-width: 768px) {
.bg {
background-image: url(bg768.jpg);
}
}
3️⃣ Оптимальный размер изображения
Не имеет смысла загружать большое изображение, если оно показывается на сайте как маленькое. Правильная стратегия — загружать миниатюру для отображения на странице, и подгружать полноразмерное изображение только при наведении курсора или открытии модального окна.
4️⃣ Снижение качества изображений
Часто разницы между качеством 90% и 100% для JPG-файлов на глаз не видно, особенно если это фоновые изображения. Можно заранее уменьшить качество в Photoshop или использовать автоматическую компрессию:
- Плагин image-webpack-loader для webpack.
- Онлайн-сервисы сжатия изображений (например, TinyPNG).
Пример настройки в webpack.config.js:
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10000, // изображения меньше 10 КБ будут вставлены прямо в код как base64
name: 'img/[name].[hash:7].[ext]'
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true
}
}
]
}
5️⃣ Использование CSS вместо изображений
Многие визуальные элементы (тени, градиенты, рамки, формы и простые иконки) можно создать с помощью CSS, а не картинок. Это значительно уменьшает объем передаваемых файлов и ускоряет загрузку страницы.
6️⃣ Использование формата WebP
WebP — современный формат изображений от Google. Плюсы WebP:
- При том же качестве картинка весит меньше, чем JPG или PNG.
- Поддерживает как сжатие с потерями, так и без потерь.
- Поддерживает прозрачность и анимацию.
Одна и та же картинка в WebP будет весить на 25-70% меньше по сравнению с JPG или PNG.
10. Загружайте код по мере необходимости через Webpack, извлекайте сторонние библиотеки, сокращайте избыточный код при преобразовании ES6 в ES5
1️⃣ Ленивая загрузка по требованию
Для реализации загрузки по требованию можно использовать динамический импорт компонентов с помощью import()
в JavaScript. В Webpack для этого можно настроить генерацию уникальных имен файлов на основе содержимого, используя contenthash
.
Конфигурация Webpack:
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
path: path.resolve(__dirname, '../dist'),
}
2️⃣ Извлечение сторонних библиотек
Сторонние библиотеки, как правило, не изменяются часто, поэтому их лучше извлекать в отдельные кэшируемые файлы, чтобы ускорить повторные загрузки страницы. Для этого используется параметр cacheGroups
в плагине splitChunks в Webpack:
optimization: {
runtimeChunk: {
name: 'manifest' // Разделяем runtime Webpack в отдельный файл.
},
splitChunks: {
cacheGroups: {
vendor: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
},
}
}
3️⃣ Сокращение избыточного кода при преобразовании ES6 в ES5
При использовании Babel для трансформации кода с ES6 в ES5 часто генерируются вспомогательные функции. Например, этот класс:
class Person {}
Преобразуется в:
"use strict";
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Person = function Person() {
_classCallCheck(this, Person);
};
Здесь _classCallCheck
— это вспомогательная функция, которая будет сгенерирована для каждого класса. Если классов много, код может стать избыточным. Чтобы избежать повторного генерирования этих функций, можно использовать пакет @babel/runtime, который объявляет все необходимые вспомогательные функции. Здесь функции больше не генерируются, а просто импортируются из @babel/runtime:
"use strict";
var _classCallCheck2 = require("@babel/runtime/helpers/classCallCheck");
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
var Person = function Person() {
(0, _classCallCheck3.default)(this, Person);
};
11. Снижайте число перерисовок и перекрасок
Перерисовка (reflow) — это процесс, при котором браузер пересчитывает расположение и размеры DOM-элементов. Когда изменяется положение или размер элементов на странице, браузер должен заново сгенерировать рендер-дерево. Это ресурсозатратная операция, потому что она требует перерасчета всех элементов на странице и их перерисовки. Примеры операций, вызывающих reflow:
- Добавление или удаление видимых DOM-элементов.
- Изменение позиции элемента (например, через
position
,left
,top
). - Изменение размеров элементов (например, через
width
,height
). - Изменение содержимого элемента (например, изменение текста).
- Изменение размера окна браузера.
Перекраска (repaint) — это процесс, при котором браузер рисует элементы на экране после того, как рендер-дерево было обновлено. В отличие от reflow, перекраска не влияет на расположение или размеры элементов, а только обновляет их визуальное представление.
Примеры операций, вызывающих repaint (но не reflow):
- Изменение цвета фона.
- Изменение цвета шрифта.
- Изменение прозрачности (через
opacity
).
ℹ️ Важно! Reflow всегда вызывает repaint, но repaint не вызывает reflow.
Как уменьшить количество перерисовок и перекрасок:
- Избегайте прямых изменений стилей через JavaScript. Вместо этого меняйте классы элементов. Изменение класса позволяет браузеру обработать стиль через каскад, что более эффективно.
- Сгруппируйте изменения DOM-элементов. Если нужно выполнить несколько операций с элементом, лучше временно удалить его из потока документа (используя
display: none
илиDocumentFragment
), затем выполнить все изменения и вернуть элемент в документ. Это предотвратит несколько перерисовок, объединяя все изменения в одну.
Пример с использованием display: none
:
let element = document.getElementById('myElement');
element.style.display = 'none'; // Убираем элемент из потока документа
// Выполняем все изменения
element.style.display = ''; // Возвращаем элемент обратно
Пример с использованием DocumentFragment
:
let fragment = document.createDocumentFragment();
fragment.appendChild(element);
// Выполняем все изменения с элементом в фрагменте
document.body.appendChild(fragment); // Вставляем все изменения в DOM
12. Используйте делегирование событий
Делегирование событий — это метод, который использует механизм всплытия событий в DOM, позволяя привязать обработчик события к родительскому элементу, а не к каждому дочернему элементу отдельно. Этот подход позволяет обрабатывать события для нескольких элементов с помощью одного обработчика, что уменьшает количество необходимых обработчиков и может сэкономить память.
Допустим, у нас есть список:
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Pineapple</li>
</ul>
Хороший пример с делегированием событий
Здесь обработчик события назначается на родительский элемент <ul>
. Когда происходит клик на любом из <li>
, событие всплывает до <ul>
, и обработчик проверяет, был ли клик сделан именно по элементу списка LI
. Это значительно экономит память, так как не нужно назначать обработчики на каждый отдельный элемент списка.
document.querySelector('ul').onclick = (event) => {
const target = event.target; // Получаем элемент, на который кликнули
if (target.nodeName === 'LI') { // Проверяем, что это элемент списка
console.log(target.innerHTML); // Выводим содержимое элемента
}
}
Плохой пример (без делегирования)
В этом случае обработчик события назначается на каждый элемент списка <li>
, что может быть неэффективно, особенно если элементов много. При добавлении новых элементов в список придется заново привязывать обработчики, что увеличивает нагрузку на браузер и память:
document.querySelectorAll('li').forEach((e) => {
e.onclick = function() {
console.log(this.innerHTML);
}
});
13. Обращайте внимание на локальность программы
Локальность в контексте программирования — это принцип, согласно которому данные, к которым часто обращаются, находятся рядом с недавно использованными данными или сами эти данные. Программы с хорошей локальностью работают намного быстрее, чем те, у которых локальность плохая.
Локальность обычно проявляется в двух формах:
- Временная локальность. Если определeнная ячейка памяти была использована, то она с высокой вероятностью будет использована снова в ближайшем будущем. То есть, данные, к которым программа обращалась недавно, с большой вероятностью будут использоваться снова.
- Пространственная локальность. Если данные из определeнной ячейки памяти были использованы, то программа с высокой вероятностью будет обращаться к данным, расположенным рядом с ними.
Пример временной локальности
Здесь переменная sum
используется в каждой итерации цикла, что является хорошим примером временной локальности. Это означает, что переменная sum будет часто использоваться, и данные, связанные с ней, будут кэшироваться, что улучшает производительность:
function sum(arry) {
let i, sum = 0;
let len = arry.length;
for (i = 0; i < len; i++) {
sum += arry[i]; // Обращаемся к arry[i] в каждой итерации
}
return sum;
}
Примеры пространственной локальности
Программа с хорошей пространственной локальностью:
// Двумерный массив
function sum1(arry, rows, cols) {
let i, j, sum = 0;
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++) {
sum += arry[i][j]; // Последовательный доступ к элементам массива по строкам
}
}
return sum;
}
Программа с плохой пространственной локальностью:
// Двумерный массив
function sum2(arry, rows, cols) {
let i, j, sum = 0;
for (j = 0; j < cols; j++) {
for (i = 0; i < rows; i++) {
sum += arry[i][j]; // Доступ к элементам по столбцам
}
}
return sum;
}
Разница между этими примерами:
- В первом примере массив сканируется по строкам, и каждый элемент в строке обрабатывается последовательно, что соответствует хорошей пространственной локальности. Массив в памяти хранится построчно, и такой доступ использует соседние ячейки памяти, что оптимально.
- Во втором примере сканирование происходит по столбцам, что приводит к плохой пространственной локальности, так как в этом случае данные, расположенные рядом в памяти, обрабатываются не последовательно.
14. Используйте switch вместо if-else
Когда количество условий в блоке if-else
становится большим, использование switch
становится более предпочтительным.
Пример с if-else
В этом примере мы проверяем значение переменной color
через несколько условий else if
. Этот подход хорошо работает, но с увеличением количества условий код становится очень громоздким и сложным для восприятия:
if (color == 'blue') {
} else if (color == 'yellow') {
} else if (color == 'white') {
} else if (color == 'black') {
} else if (color == 'green') {
} else if (color == 'orange') {
} else if (color == 'pink') {
}
Пример со switch
В этом примере мы используем конструкцию switch
, которая делает код более читабельным, особенно если условий много. Каждое условие указано отдельно, и легко понять, какие действия выполняются для каждого конкретного случая:
switch (color) {
case 'blue':
// действия для синиго цвета
break;
case 'yellow':
// действия для жёлтого цвета
break;
case 'white':
// действия для белого цвета
break;
case 'black':
// действия для чёрного цвета
break;
case 'green':
// действия для зелёного цвета
break;
case 'orange':
// действия для оранжевого цвета
break;
case 'pink':
// действия для розового цвета
break;
}
15. Используйте таблицы поиска
Когда количество условных операторов (например, switch
или if-else
) становится слишком большим, использование этих конструкций может стать неэффективным и трудным для поддержания. В таких случаях можно воспользоваться таблицами поиска (lookup tables), которые можно реализовать с использованием массивов или объектов.
Пример со switch
switch (index) {
case '0':
return result0;
case '1':
return result1;
case '2':
return result2;
case '3':
return result3;
case '4':
return result4;
case '5':
return result5;
case '6':
return result6;
case '7':
return result7;
case '8':
return result8;
case '9':
return result9;
case '10':
return result10;
case '11':
return result11;
}
Таблица поиска с использованием массива
Здесь мы создаeм массив results
, в котором все возможные результаты хранятся в соответствующих индексах. Таким образом, вместо использования множества case
в switch
, мы просто обращаемся к элементу массива по индексу, что делает код компактным и быстрым:
const results = [result0, result1, result2, result3, result4, result5, result6, result7, result8, result9, result10, result11];
return results[index];
Пример с объектом
Если ключи условий являются не числами, а строками, то вместо массива для построения таблицы поиска можно использовать объект. Здесь мы создаем объект map
, где ключами являются строки (например, "red"
, "green"
), а значениями — результаты, которые нужно вернуть. Вместо использования нескольких условных операторов, мы просто обращаемся к объекту с нужным ключом (в данном случае — значением переменной color
):
const map = {
red: result0,
green: result1,
};
return map[color];
16. Избегайте подергиваний страницы
Сегодня большинство устройств имеют частоту обновления экрана 60 раз в секунду (60 Hz). Это означает, что браузер должен рендерить анимации или эффекты на странице, а также обрабатывать прокрутку, чтобы они синхронизировались с этой частотой обновления экрана.
Время, отведенное на каждый кадр, составляет чуть больше 16 миллисекунд (1 секунда / 60 = 16,66 миллисекунд). Однако на практике браузер выполняет дополнительные операции (например, обработка событий, выполнение скриптов), и все работы по рендерингу должны быть завершены в пределах 10 миллисекунд. Если это время превышается, частота кадров снижается, и контент начинает дергаться на экране. Это явление очень негативно влияет на пользовательский опыт.
Чтобы избежать таких проблем, необходимо позаботиться о том, чтобы все операции выполнялись быстро, а если какие-то задачи выполняются долго, их можно разбить на несколько этапов, чтобы не блокировать основной поток выполнения.
Пример с разделением работы
Предположим, у вас есть цикл, который выполняет сложную операцию для каждого элемента массива, и этот процесс слишком долгий:
for (let i = 0, len = arry.length; i < len; i++) {
process(arry[i]);
}
Вместо того, чтобы выполнять все операции за один раз, можно разбить их на более мелкие задачи, чтобы не перегружать процесс рендеринга. В этом примере задачи выполняются с паузой в 25 миллисекунд, что позволяет браузеру иметь достаточно времени для обновления экрана и предотвращает подергивание страницы:
const todo = arry.concat();
setTimeout(function() {
process(todo.shift()); // Обработка одного элемента
if (todo.length) {
setTimeout(arguments.callee, 25); // Следующая итерация через 25 мс
} else {
callback(arry); // Завершение после всех операций
}
}, 25);
ℹ️ Дополнительная информация от разработчиков Google:
17. Используйте requestAnimationFrame для реализации визуальных изменений
Как мы узнали в пункте 16, большинство устройств имеют частоту обновления экрана 60 раз в секунду, и это означает, что на каждый кадр выделяется примерно 16,66 миллисекунд. Когда мы используем JavaScript для реализации анимаций, для достижения наилучших результатов важно, чтобы код выполнялся в начале каждого кадра.
Метод requestAnimationFrame
гарантирует, что анимации будут выполняться синхронно с обновлением экрана. Он позволяет браузеру оптимизировать выполнение анимаций и обеспечивает запуск кода непосредственно перед рендерингом нового кадра, что минимизирует вероятность пропуска кадров и устраняет подергивания.
Пример использования:
/**
* Если этот код выполнится как callback для requestAnimationFrame,
* он будет запущен в начале кадра.
*/
function updateScreen(time) {
// Здесь выполняются визуальные изменения.
}
requestAnimationFrame(updateScreen);
ℹ️ Если вместо requestAnimationFrame
использовать setTimeout
или setInterval
для анимации, то callback-функция может быть выполнена в любой момент внутри кадра, возможно, ближе к его концу. Это может вызвать пропуск кадров, что приведет к подергиваниям и ухудшению восприятия анимации.
ℹ️ Дополнительная информация от разработчиков Google:
18. Используйте Web Workers
Web Workers — это механизм, который позволяет выполнять задачи в отдельных потоках, независимо от основного потока JavaScript. Это позволяет выполнять длительные операции, не блокируя интерфейс пользователя. Рабочие потоки (workers) могут отправлять сообщения в основной поток JavaScript и получать сообщения от него.
Web Workers идеально подходят для обработки данных или выполнения долгих скриптов, которые не связаны с интерфейсом браузера.
Как создать новый worker
Нужно указать URI скрипта, который будет выполняться в worker-потоке:
var myWorker = new Worker('worker.js');
После этого можно отправлять сообщения в worker через метод postMessage()
и обрабатывать их с помощью события onmessage
.
Примеры работы с Web Worker
1️⃣ В основном потоке (main.js):
first.onchange = function() {
myWorker.postMessage([first.value, second.value]); // Отправляем данные в worker
console.log('Message posted to worker');
}
second.onchange = function() {
myWorker.postMessage([first.value, second.value]); // Отправляем данные в worker
console.log('Message posted to worker');
}
2️⃣ В worker.js:
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = 'Result: ' + (e.data[0] * e.data[1]); // Обработка данных
console.log('Posting message back to main script');
postMessage(workerResult); // Отправка результата обратно в основной поток
}
3️⃣ Ответ на сообщение от worker в основном потоке:
myWorker.onmessage = function(e) {
result.textContent = e.data; // Получаем данные и отображаем их в интерфейсе
console.log('Message received from worker');
}
ℹ️ Ограничения Web Workers:
- Внутри worker нельзя напрямую манипулировать DOM-элементами.
- Невозможно использовать стандартные методы и свойства объекта
window
в worker. Однако, можно использовать другие возможности, такие как WebSockets, IndexedDB, и API хранения данных, специфичные для некоторых платформ, например, для Firefox OS.
ℹ️ Дополнительная информация от разработчиков Mozilla:
19. Применяйте побитовые операции
В JavaScript числа хранятся в 64-битном формате по стандарту IEEE-754. Однако при выполнении побитовых операций числа конвертируются в 32-битный формат со знаком. Несмотря на это, побитовые операции гораздо быстрее, чем другие математические или логические операции.
Основные типы побитовых операций:
1️⃣ Операция по модулю
Последний бит четных чисел всегда равен 0, а у нечетных чисел — 1. Это свойство позволяет заменить операцию взятия по модулю на побитовую операцию. Например, этот код:
if (value % 2) {
// Нечетное число
} else {
// Четное число
}
Можно заменить на побитовую операцию:
if (value & 1) {
// Нечетное число
} else {
// Четное число
}
Операция & 1
проверяет последний бит числа. Если это 1 (для нечетных чисел), то результат будет истинным (true
).
2️⃣ Округление вниз
Операция ~~
используется для округления числа вниз до ближайшего целого. Например, здесь ~~
действует как эквивалент функции Math.floor()
, но быстрее:
~~10.12 // 10
~~10 // 10
~~'1.5' // 1
~~undefined // 0
~~null // 0
3️⃣ Битовая маска
С помощью побитовых операций можно создавать маски и проверять, включен ли тот или иной флаг в наборе опций:
const a = 1;
const b = 2;
const c = 4;
const options = a | b | c; // Используем побитовую операцию ИЛИ, чтобы установить флаги.
Теперь, чтобы проверить, включен ли флаг b
в options, используем побитовую операцию И:
// Есть ли флаг b в options?
if (b & options) {
// Если результат ненулевой, значит флаг b установлен
...
}
20. Не переопределяйте встроенные методы
Независимо от того, насколько оптимизирован ваш JavaScript код, он не может сравниться с производительностью встроенных методов. Это связано с тем, что встроенные методы написаны на низкоуровневых языках (таких как C или C++) и компилируются в машинный код, который является частью браузера.
Когда встроенные методы доступны, старайтесь использовать их вместо написания собственных решений, особенно для таких операций, как математические вычисления и манипуляции с DOM. Например:
- Математические операции (
Math.pow()
,Math.sqrt()
и другие) работают гораздо быстрее, чем аналогичные реализации на чистом JavaScript. - Манипуляции с DOM (методы
document.querySelector()
,getElementById()
,setAttribute()
и другие) оптимизированы для работы с низкоуровневыми операциями и работают значительно быстрее, чем любые пользовательские функции, которые пытаются имитировать их поведение.
21. Снижайте сложность CSS-селекторов
Когда браузеры считывают CSS-селекторы, они делают это справа налево. Например, у вас есть такой селектор:
#block .text p {
color: red;
}
Браузер будет проверять элементы в таком порядке:
- Сначала найти все элементы
<p>
. - Затем для каждого найденного
<p>
проверить, есть ли у него родитель с классом.text
. - И наконец, для каждого из этих
.text
проверить, есть ли у него родитель сid="block"
.
В общем, чем длиннее и запутаннее селектор, тем больше работы у браузера, особенно если селектор начинается с универсальных или общих правил.
ℹ️ Селекторы в CSS имеют определенный приоритет. Вот краткая шпаргалка по приоритетам:
- Стили в атрибуте элемента
style=""
(инлайновые в HTML) — самый высокий приоритет. - Селекторы по ID (
#block
) — высокий. - Селекторы по классу (
.text
) — средний. - Селекторы по тегу (
p
,div
и т.д.) — низкий. - Универсальный селектор
*
— самый низкий.
👉 Для повышения читаемости и поддерживаемости кода нужно помнить, что:
- Чем короче и проще селектор — тем лучше.
- Лучше использовать ID и классы, а не длинные цепочки из тегов и классов.
- Избегайте использования универсального селектора
*
(например* { margin:0; }
), так как это заставляет браузер проходить по всем элементам страницы.
22. Используйте Flexbox вместо старых способов расположения элементов
Раньше, до появления Flexbox, для верстки страниц использовали:
- Абсолютное позиционирование
position: absolute;
. - Относительное позиционирование
position: relative;
. - Флоаты
float: left;
и т. д.
Эти подходы позволяли размещать элементы на странице, но имели свои ограничения и могли вызывать проблемы, например, с выравниванием, адаптивностью или порядком элементов.
С появлением Flexbox (flex-контейнеров) все упростилось. Этот метод:
- Очень гибкий.
- Упрощает выравнивание и распределение пространства.
- Позволяет легко адаптировать макеты под разные размеры экрана.
- Работает быстрее, чем старые способы.


ℹ️ Дополнительная информация от разработчиков Google:
23. Используйте transform и opacity для создания анимаций
Когда вы анимируете элементы с помощью CSS, важно понимать, какие свойства нагружают браузер, а какие позволяют сделать анимацию плавной и быстрой.
Например, если вы анимируете свойства вроде width
, height
, margin
, padding
, top
, left
и т. д. — браузеру приходится:
- Пересчитывать размеры и положение элементов (reflow).
- Перерисовывать часть страницы (repaint).
- И только после этого отдавать результат на экран.
Это затратные операции для процессора, особенно если на странице много элементов.
👉 Вместо этого лучше использовать анимации свойств:
transform
(например:translate
,scale
,rotate
)opacity
(прозрачность)
Эти свойства обрабатываются видеокартой. В результате:
- Браузеру не нужно пересчитывать размеры или положение элементов.
- Не нужно перерисовывать все содержимое.
- Перемещение, поворот, масштабирование и изменение прозрачности происходят быстрее и анимация получается плавной, без задержек и рывков.
Плохой пример
Браузер будет выполнять reflow и repaint:
.element {
transition: left 0.5s ease;
}
Хороший пример
Браузер просто сдвигает картинку на графическом уровне, не перерисовывая весь макет:
.element {
transition: transform 0.5s ease;
}
ℹ️ Дополнительная информация от разработчиков Google:
24. Используйте правила разумно и избегайте чрезмерной оптимизации
Когда вы занимаетесь повышением производительности сайта, важно понимать, что оптимизация делится на два типа:
- Оптимизация времени загрузки — ускоряет загрузку и отображение страницы.
- Оптимизация времени выполнения — делает сайт быстрее и плавнее уже после загрузки, во время взаимодействия пользователя (например, анимации, скроллинг, клики).
Из приведенных выше рекомендаций первые 10 относятся к ускорению загрузки, а последние 13 — к ускорению работы сайта уже после загрузки. Вам не нужно применять все рекомендации сразу! Правильнее будет сначала определить проблему (что именно тормозит сайт) — иначе вы не будете понимать, что именно нужно сделать в первую очередь.
Как измерить время загрузки:
- Время белого экрана (White Screen Time) — сколько времени проходит от нажатия Enter в браузере до появления первого контент
- Время отрисовки первого экрана (First Screen Time) — сколько времени до полного отображения первого видимого блока (то, что пользователь видит без прокрутки).
Для замеров можно использовать простой скрипт, разместив его перед </head>
:
<script>
// Время белого экрана
new Date() - performance.timing.navigationStart
// Или так, более точно:
performance.timing.domLoading - performance.timing.navigationStart
</script>
А для времени отрисовки первого экрана — выполняйте замер внутри window.onload
:
window.onload = function() {
console.log(new Date() - performance.timing.navigationStart);
}
Как измерить производительность выполнения:
- Откройте сайт в Chrome.
- Нажмите F12 → вкладка Performance.
- Нажмите на серую кнопку записи (в левом верхнем углу) — она станет красной.
- Сымитируйте действия пользователя: прокрутку, клики, анимации.
- Нажмите Stop и посмотрите результаты: если на графике есть красные блоки — это признак потери кадров (зависания); если блоки зеленые — сайт работает плавно и быстро.
Заключение
Как показывает практика, большинство проблем с производительностью можно решить на ранних этапах, если заранее задуматься о том, как устроен рендеринг и как браузер «видит» ваш сайт.
Нет универсального рецепта для всех проектов, но знание этих приемов позволит вам в любой ситуации выбирать оптимальный путь, будь то грамотная загрузка изображений, аккуратная анимация или использование Web Workers. Быстрый сайт — довольный пользователь. Медленный сайт — закрытая вкладка. Все просто!
Комментарии