Оптимизация сайта на Angular

Дано: интернет-магазин на Angular 5, не слишком мелкий, около 10 тыс. товаров, видна работа над SEO, есть довольно большие информационные разделы. Google PageSpeed Insights показывает от 33 до 59 на разных страницах. Настроен серверный рендеринг.

Нужно: сделать максимально хорошо.

Были выбраны такие направления оптимизации:

  1. Отложенная загрузка модулей Angular для уменьшения количества исполняемого js кода на странице.
  2. Оптимизация зависимостей.
  3. Отложенная загрузка части css.
  4. Уменьшение количества неиспользуемого css на странице.

Отложенная загрузка модулей Angular

При разработке на Angular нужно стараться дробить код на модули так, чтобы отдельные страницы были оформлены в отдельные модули. При этом компоненты, которые повторно используются в разных модулях, тоже нужно выделять в отдельные модули.

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

Выглядит это примерно так:

Выделяем компонент отдельной страницы CustomerListComponent в отдельный модуль:

import { NgModule } from '@angular/core';
import { RoutesRouterModule } from '@angular/router';
import { CustomerListComponent } from './customer-list/customer-list.component';
// Обратите внимание на пустой маршрут!
const routesRoutes = [
  {
    path'',
    componentCustomerListComponent
  }
];
// Обратите внимание на то, что в это модуле RouterModule инициализируется 
// .forChild(), а в главном модуле — .forRoot()
@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class CustomersRoutingModule { }

Из основного модуля убираем этот компонент, модуль но подключаем, но добавляем маршрут с отложенной загрузкой:

// Обратите внимание на маршрут 'customers',
// именно по нему будет доступен наш новый модуль
const routesRoutes = [
  {
    path'customers',
    loadChildren'./customers/customers.module#CustomersModule'
  },
  /** ... другие маршруты ... **/
];

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

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

Делается это так:

RouterModule.forRoot(routes,
    {
        /** Предзагрузка всех модулей после показа главной **/
        preloadingStrategyPreloadAllModules,
        initialNavigation'enabled'
    }
)

Тут есть еще серьезный момент с подключением сервисов rxjs сообщений. Сервисы сообщений, которые используются в нескольких модулях, нужно подключать один раз в главном модуле. Если подключить такой сервис 2 раза в разных модулях, некоторые сообщения не приходят или приходят непредсказуемо.

Оптимизация js зависимостей

Тут все довольно просто. Убираем те зависимости, которые не используются. Желательно также избавиться от тех, без которых легко обойтись. В моём случае, например, пришлось избавиться от пакета ngx-spinner и заменить его на обычный спиннер на базе font awesome (fa fa-spin fa-spinner).

Отложенная загрузка css

На любом сайте есть критичный css, сильно влияющий на отрисовку страницы (например, bootstrap grid), и остальной некритичный css. Обычно критичным считают тот css, который обеспечивает нормальный показ верхней части страницы, которую видно сразу после загрузки, без скролла вниз.

Отложенная загрузка некритичных css способна сильно ускорить показ страницы при загрузке.

Angular cli позволяет не включать определенные css в билд:

// В файле .angular-cli.json
"styles": {
...
        "./assets/scss/bootstrap.inline.scss",
        {
            "input""./assets/scss/bootstrap.lazy.scss",
            "lazy"true,
            "output""bootstrap-lazy"
        }
...
}

В этом примере bootstrap.inline.scss включается в билд, и загружается немедленно, а bootstrap.lazy.scss — нет. Скомпилированный из bootstrap.lazy.scss файл, тем не менее, появится в папке dist.

Подключить этот файл мы должны самостоятельно, в тот момент, когда считаем нужным. Сделать это можно так:

export class AppComponent implements OnInit {
  ngOnInit() {
    this.loadExternalStyles('bootstrap-lazy.bundle.css').then(() => {}).catch(() => {});
  }
  private loadExternalStyles(styleUrlstring) {
    return new Promise((resolvereject=> {
      const styleElement = document.createElement('link');
      styleElement.href = styleUrl;
      styleElement.rel = 'stylesheet';
      styleElement.type = 'text/css';
      document.head.appendChild(styleElement);
    });
  }
}

Если мы не хэшируем файлы билда, то на этом проблема решена, и все хорошо. Но если хэшируем (параметр –output-hashing), то после билда в папке dist появляется файл с непредсказуемым названием bootstrap-lazy.ccb74eb3500187878a64.bundle.css, которое меняется после каждого билда, поэтому мы не можем его загрузить.

Решение этой проблемы в таком патче (спасибо stackoverflow):

// patch-ng-cli.js
const fs = require('fs');
const stylesFileToPatch = "node_modules/@angular/cli/models/webpack-configs/styles.js";
const regex = /extraPlugins\.push\(.*\}\)\)\;/;
const patchContent = `
        // PATCHED CONTENT START
        const globalStyles = utils_1.extraEntryParser(appConfig.styles, appRoot, 'styles');
        extraPlugins.push(new ExtractTextPlugin({ filename: getPath => {
            const generatedFileName = getPath(\`[name]\${hashFormat.extract}.bundle.css\`);
            const name = generatedFileName.split(".")[0];
            const globalAppStylesConfigEntry = globalStyles.find(path => path.output === name);
            if (globalAppStylesConfigEntry && globalAppStylesConfigEntry.lazy){
                console.log(\`\${name} will not be hashed due to lazy loading\`);
                return \`\${name}.bundle.css\`
            }
            console.log(generatedFileName);
            return generatedFileName;
        }}));
        // PATCHED CONTENT END
`;
fs.readFile(stylesFileToPatch, (errdata=> {
    if (err) { throw err; }
    const text = data.toString();
    const isAlreadyPatched = !!text.match("PATCHED CONTENT");
    if (isAlreadyPatchedreturn console.warn("-- already patched --"stylesFileToPatch);
    console.log('-- Patching ng-cli: 'stylesFileToPatch);
    const patchedContent = text.replace(regexpatchContent);
    const file = fs.openSync(stylesFileToPatch'r+');
    fs.writeFile(filepatchedContent, () => console.log("-- Patching -- OK"));
    fs.close(file);
});

Установка патча в package.json:

// package.json
{
...
    "scripts": {
    ...
        "postinstall""node ./patch-ng-cli.js"
    },
...
}

Чтобы патч отработал, нужно запустить установку (yarn install).

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

Если на локали, при запуске через ng serve, отложенная загрузка css не будет работать, то стоит попробовать запускать сайте через npm run start, а саму команду start модифицировать таким образом:

// package.json
{
...
    "scripts": {
    ...
        "start""ng build -op dist --extract-css -w"
    },
...
}

Тут также нужно не забыть запустить сам сервер (через node server.js или через pm2, или любым другим способом).

Уменьшение количества неиспользуемого css на странице

Часто при использовании bootstrap-а люди забывают отключить его неиспользуемые модули. Порочной также является практика подключить весь бутстрап, а потом переопределять его стили, подгоняя внешний вид сайта под желаемый. Bootstrap отлично настраивается! Кроме того, полезно может быть поисследовать другие подключенные библиотеки на предмет лишнего css, который можно отключить.

В результате

В результате всех действий сайт стал явно лучше работать на слабых устройствах и в условиях плохого интернета. Баллы Google PageSpeed Insights поднялись до 65 — 75, что явно лучше, чем было (33 — 59).

Однако, расти всегда есть куда! У меня, например, осталось еще довольно много лишнего css, который хорошо бы было убрать. Есть также пара лишних зависимостей, от которых хорошо бы в будущем избавиться.

За рамками статьи оказалась такая важная тема, как отложенная загрузка и оптимизация изображений. Может быть, в другой раз.