Как оптимизировать приложения на Angular

Angular — это наиболее популярный и предпочитаемый большинством фреймворк для создания одностраничных приложений. Хотя я и упомянул только про одностраничные приложения, это вовсе не означает, что в Angular нельзя создавать сайты с десятками страниц. Сам фреймворк (последние версии) очень хорошо оптимизирован, благодаря команде профессионалов и сплоченному сообществу, однако, когда дело касается производительности, мы всегда должны думать о некоторых вещах, которые сделают ваше приложение еще более быстрым и плавным.

#1 Оптимизация с помощью ленивой загрузки

Когда мы создаем наше приложение без использования ленивой загрузки, скорее всего мы увидим вот такие сгенерированные файлы в папке dist:

polyfills.js
scripts.js
runtime.js
styles.css
main.js

polyfills.js нужен для того, чтобы приложение было совместимо с различными браузерами, так как написанный нами код содержит множество всевозможных функций и не все браузеры их поддерживают.

scripts.js содержит в себе скрипты, которые мы объявляем в разделе scripts файла angular.json

"scripts": [
"myScript.js",
]

runtime.js — это webpack загрузчик. Данный файл содержит webpack утилиты, необходимые для загрузки других файлов.

styles.css содержит все стили, которые мы объявляем в разделе styles файла angular.json

"styles": [
"src/styles.css",
"src/my-custom.css"
],

main.js содержит весь наш код, включая компоненты (код ts, html и css), каналы (pipes), директивы, сервисы и все другие импортированные модули (в том числе сторонние).

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

Самый простой способ решения этой проблемы — разделить код нашего приложения на несколько разных кусков (модулей) и загружать их “лениво”, то есть по мере необходимости. Загрузка «ленивая», потому что мы загружаем не все приложение, а лишь определенную его часть. В Angular ленивая загрузка осуществляется через route.

Для примера я создал два компонента: app.component и second.component. Оба находятся в app.module, так что ничего “ленивого” нет. App.component крайне прост, так как имеет всего две кнопки: для перехода в Second.component и обратно в App.component.

Однако second.component содержит в себе очень большой текст (около 1 мб).

Поскольку мы не используем ленивую загрузку, когда создаем приложение, мы получаем большой main.js, который содержит в себе код, как от app.component, так и от second.component.

Мы можем увидеть размер main.js (1.3 мб — это много) во вкладке Network в Chrome DevTools.

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

При использовании ленивой загрузки, после процесса сборки будет создан новый файл, например, 4.386205799sfghe4.js. Это и есть тот ленивый модуль, который загружается только при переходе на подходящую страницу. Теперь, когда мы открываем приложение, мы видим, что main.js очень маленький (266 кб).

И только когда мы переходим на вторую страницу, мы видим, что новый файл (1 мб) загружен.

Однако загрузка сайта таким образом все равно скажется на производительности, поскольку загрузка отдельной страницы все равно будет относительно медленной в первый раз. К счастью, Angular предоставляет способ решения этой проблемы с помощью PreloadingStrategy. Мы можем сказать Angular, чтобы он полностью загрузил и выполнил наш основной модуль (main.js), а затем загрузил другие ленивые модули в фоновом режиме, чтобы при переходе на ленивые страницы все было уже подгружено. Пример кода для предварительной загрузки всех модулей:

import { PreloadAllModules, RouterModule } from '@angular/router';
RouterModule.forRoot(
[
{
path: 'second',
loadChildren: './second/second.module#SecondModule'
} 
], {preloadingStrategy: PreloadAllModules})
Старайтесь использовать как можно больше ленивых модулей совместно с PreloadingStrategy. Это позволит сохранить ваш main.js маленького размера, что означает более быструю загрузку и отображение главной страницы.

#2 Оптимизация с помощью Webpack Bundle Analyzer

Если после предыдущего способа оптимизации у вас все равно получается большой main.js (большим я считаю размер более 1 мб для небольших приложений), вы можете воспользоваться Webpack Bundle Analyzer. Он дает возможность визуализировать размер полученных webpack файлов через интерактивный масштабируемый Treemap. Прежде всего, установите данный плагин как зависимость dev в своем Angular проекте:

npm install --save-dev webpack-bundle-analyzer

Затем измените файл package.json, добавив такую строку в разделе scripts

"bundle-report": "ng build --prod --stats-json && webpack-bundle-analyzer dist/stats.json"

Обратите внимание на то, что dist/stats.json может отличаться в вашем случае. Например, если ваши файлы генерируются в dist/browser, вам необходимо изменить вышеприведенную строку на dist/browser/stats.json.

Наконец, запускаем:

npm run bundle-report

У нас получится финальный билд со статистикой о каждом bundle, и с помощью Webpack Bundle Analyzer мы сможем визуализировать все это волшебство, посредством масштабируемого treemap.

Здесь мы можем увидеть, какие модули/файлы находятся в каждом bundle. Это чрезвычайно полезно, поскольку мы можем визуально увидеть, что нужно оставить, а чего быть не должно.

#3 Оптимизация с помощью создания нескольких общих модулей

Для данной задачи принято использовать такой принцип программирования, как DRY — don’t repeat yourself (не повторяйте себя), но иногда общие модули становятся слишком большими. Например, у нас есть SharedModule, который содержит в себе много других модулей/компонентов/пайпов. Импорт такого модуля в app.module увеличит размер main.js, потому что мы будем импортировать не только то, что нужно main.js, но и все остальное, что идет вместе с SharedModule. Чтобы избежать этого, мы можем создать еще один общий модуль, например, HomeSharedModule, который будет содержать только те компоненты, которые необходимы main.js и его компонентам.

Наличие нескольких общих модулей лучше, чем один большой общий модуль.

#4 Используйте ленивую загрузку для изображений, которые не отображаются на странице

Когда мы в первый раз открываем главную страницу нашего сайта, мы можем заметить изображения, которые видны не до конца (то есть изображения, не попавшие в область просмотра). Пользователь должен прокрутить страницу вниз, чтобы увидеть эти изображения. Хотя эти изображения и не видны до конца, они все равно полностью подгружаются, как только мы открываем главную страницу сайта. И если на странице присутствует большое количество изображений — это может сильно сказаться на производительности. Для решения этой проблемы мы можем вновь воспользоваться ленивой загрузкой: изображение будут загружаться только в том момент, когда пользователь дойдет до него. Существует JavaScript API — Intersection Observer API, который в разы упрощает ленивую загрузку изображений. Кроме того, мы можем создать директиву для многократного использования.

#5 Используйте виртуальный скроллинг для длинных списков

7 версия Angular принесла с собой обновление для CDK в виде “виртуального скроллинга”. Виртуальный скроллинг загружает и выгружает элементы из DOM на основе видимых частей списка, что делает наше приложение чрезвычайно быстрым.

Вместо загрузки и отображения полного списка, мы можем отобразить только те элементы, которые видны в это время

#6 Используйте FOUT вместо FOIT для шрифтов

В основном, на веб-сайтах используются красивые пользовательские шрифты, а не обычные стандартные шрифты. Однако использование пользовательских шрифтов или шрифтов, предоставляемых другой службой, требует, чтобы браузер загружал и обрабатывал эти шрифты, когда пользователь посещает нашу страницу. Существует два сценария того, что произойдет, если мы будем использовать дизайнерские шрифты, предоставляемые сторонними сервисами, такими как Google Fonts:
 1. Браузер загружает шрифт в нужный момент, обрабатывает его и только затем отображает текст на странице. Текст на странице будет невидим до тех пор, пока шрифт не будет загружен и обработан. Это называется FOIT или Flash of invisible text(мелькание невидимого текста).

2. Сначала браузер отображает текст в стандартном шрифте и пытается получить внешние стили шрифтов. Затем при загрузке и обработке он поменяет стандартный шрифт на наш пользовательский. Текст на странице будет отображаться обычным шрифтом до тех пор, пока браузер не загрузит и не проанализирует внешний шрифт, чтобы заменить им стандартный. Это называется FOUT или Flash of unstyled text(мелькание текста без стилей).

Большинство браузеров используют FOIT, и только Internet Explorer использует FOUT. Чтобы исправить это, мы можем использовать font-display дескриптор для @font-face и сообщить браузеру, что мы хотим использовать стандартный шрифт, а затем поменять его на пользовательский или мы хотим оставить текст невидимым.


Существуют и другие способы повышения производительности, включая Server Side Rendering (отрисовка на стороне сервера), Service Worker, AMP страницы (технология ускоренных мобильных страниц) и многое другое.

Перевод статьи J Stepanyan: How to optimize Angular applications