Быстрое и прогрессивное веб-приложение является обязательным стандартом для недавней интерфейсной разработки. Встретить прогрессивность и производительность порой становится не так-то просто. Одностраничное приложение обычно поставляется с тяжелым пакетом кода JavaScript. Устройство клиента должно загрузить его, прежде чем приложение станет интерактивным.

Google пытается помочь разработчикам оценить производительность приложений с помощью утилиты Lighthouse. Его можно найти на вкладке Audits панели инструментов разработчика Google Chrome. Ниже приведены некоторые базовые методы, которые я использовал, чтобы получить высокий балл, например, приложение, основанное на стеке React, Redux и Material-UI.

Разделите пакет кода

Многие инструменты веб-связывания, такие как Grunt, Gulp и другие, могут справиться с этой задачей. Webpack делает это более элегантным и простым способом, предоставляя множество функций и техник.

Во-первых, отделите тяжелый код поставщика от файла поставщика:

entry: {
  app: ['./src/bootstrap.js'],
  vendor: [
    'react',
    'react-dom',
    'redux',
    'react-redux',
    'redux-thunk',
    'react-router-dom',
    'prop-types',
    'jss',
    'axios',
  ],
},

Чтобы понять, какие библиотеки можно переселить, попробуйте отличный пакет webpack-bundle-analyzer. Мы также должны сказать webpack для сбора данных о поставщиках из активов:

plugins: [
  ...
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    filename: 'js/vendor.js',
    minChunks: Infinity,
  }),
  ...
],

Затем переместите независимые компоненты в отдельные файлы фрагментов. Вы можете определить для них новые записи веб-пакетов или включить их в свой код с помощью плагина bundle-loader:

import SignIn from 'bundle-loader!user/containers/SignIn';

Наконец, мы можем попросить webpack найти общий код в наших кусках и также разделить его:

plugins: [
  ...
  new webpack.optimize.CommonsChunkPlugin({
    name: 'meta',
    filename: 'js/meta.js',
    minChunks: 2,
  }),
  ...
],

Используйте отложенную загрузку

Ленивая загрузка - один из полезных методов оптимизации производительности. Наши фрагменты, включенные загрузчиком пакетов, будут загружены при инициализации маршрутизатора. Мы можем использовать трюк, чтобы лениво загружать их по запросу. Вместо того, чтобы указывать маршрут к компоненту фрагмента, мы можем указать его на компонент-оболочку пакета:

import { Component } from 'react';
import PropTypes from 'prop-types';

class BundleComponent extends Component {
  static propTypes = {
    loader: PropTypes.func.isRequired,
    children: PropTypes.func.isRequired,
  };

  state = {
    mod: null,
  };

  componentWillMount() {
    this.load(this.props);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.loader !== this.props.loader) {
      this.load(nextProps);
    }
  }

  load = ({ loader }) => {
    this.setState({
      mod: null,
    });
    loader((mod) => {
      this.setState({
        mod: mod.default ? mod.default : mod,
      })
    });
  };

  render() {
    return this.state.mod
      ? this.props.children(this.state.mod)
      : <span />;
  }
}

export default BundleLoader => props => (
  <BundleComponent loader={BundleLoader}>
    {LoadedComponent => <LoadedComponent {...props} />}
  </BundleComponent>
);

Затем мы можем использовать оболочку пакета для инициализации маршрутизатора:

import bundle from './bundle';
import RegLoader from 'bundle-loader?lazy!user/containers/Registration';
import SignInLoader from 'bundle-loader?lazy!user/containers/SignIn';
const AppRoutes = () => (
  <Router>
    <App>
      <Route path='/register' component={bundle(RegLoader)} />
      <Route path='/login' component={bundle(SignInLoader)} />
    </App>
  </Router>
);

Также рекомендуется загружать содержимое компонента после рендеринга самого компонента:

import AppBarContentLoader from 'bundle-loader?lazy!appbar/components/AppBarContent';
const AppBarContent = bundle(AppBarContentLoader);

const Layout = ({ children }) => {
  return (
    <div className='wrapper'>
      <AppBar>
        <AppBarContent />
      </AppBar>

      <div className='content'>
        {children}
      </div>
    </div>
  );
};

Использовать JSS

Одностраничное приложение должно загружать статический CSS перед начальным рендерингом. Это означает, что мы должны загрузить более 100 Кбайт, увеличивая начальное время взаимодействия. Мы можем добиться большего с JSS, включая только необходимые стили, только когда они действительно нужны:

import { withStyles } from 'material-ui/styles';

const styleSheet = theme => ({
  root: {
    display: 'flex',
    minHeight: '100vh',
  },
  content: {
    flex: '1 1 100%',
    margin: '0 auto',
  },
  appBar: {
    minHeight: 56,
  },
});

const Layout = ({ children, classes }) => {
  return (
    <div className={classes.root}>
      <AppBar className={classes.appBar}>
        <AppBarContent />
      </AppBar>

      <div className={classes.content}>
        {children}
      </div>
    </div>
  );
};

export default withStyles(styleSheet)(Layout);

Внедрить Service Worker

Service Worker - это легкое и мощное промежуточное ПО между приложением и сервером. Он использует Javascript и позволяет контролировать любой запрос и ответ. Для нас это означает, что мы можем контролировать, какие активы могут быть кэшированы, когда и почему. В качестве дополнительного приложения мы можем обрабатывать запросы к серверу, когда сеть недоступна. Затем отправьте эти запросы, когда приложение появится в сети.

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

  • хранить только GET-запросы к нашему серверу;
  • исключить запросы к API для получения актуальных данных;
  • хэширование статических ресурсов (CSS, js, шрифты и т. д.) по содержимому и сохранение их в постоянном хранилище;
  • хранить ответы HTML во временном хранилище до следующей сборки приложения.

Получение статических имен активов, хешированных по содержимому, может быть выполнено с помощью Webpack:

output: {
  path: path.join(__dirname, '/public/'),
  filename: 'js/[name].[chunkhash].js',
  chunkFilename: 'js/[name].[chunkhash].js',
  publicPath: '/',
},

В этом случае мы не отслеживаем обновления файлов, если файла нет в кеше, мы должны его кешировать:

// Hash: {hash}
var PERMANENT_STORAGE = 'react-pwa-perm';
var TEMP_STORAGE = 'react-pwa-temp';
var permanentUrlTemplates = [
  '/js/',
  '/fonts/',
  '/styles/'
];
var urlsToInstall = ['{files_to_cache}'];
var hostnameToCache = '{hostname}';
var hostnameExcludeFromCache = '{api_hostname}';

self.addEventListener('fetch', function(event) {
  if (event.request.method !== 'GET' ||
    event.request.url.indexOf(hostnameToCache) === -1 ||
    event.request.url.indexOf(hostnameExcludeFromCache) !== -1
  ) {
    return;
  }
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          return response;
        }

        var fetchRequest = event.request.clone();

        return fetch(fetchRequest).then(
          function(response) {
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            var responseToCache = response.clone();

            var isPermanent = false;
            permanentUrlTemplates.map(function(template) {
              if (event.request.url.indexOf(template) !== -1) {
                isPermanent = true;
              }
            });

            caches.open(isPermanent 
              ? PERMANENT_STORAGE
              : TEMP_STORAGE
            ).then(function(cache) {
              cache.put(event.request, responseToCache);
            });

            return response;
          }
        );
      })
  );
});

При первой загрузке необходимо установить сервис-воркер. Чтобы сделать это правильно, мы должны предоставить список ресурсов для кеширования. Service worker регистрирует свой экземпляр только после успешного кэширования. Но наши активы хешируются, и мы можем получить хешированные имена файлов только на этапе сборки веб-пакета. Чтобы помочь нам внедрить хешированные ресурсы, мы можем использовать inject-assets-webpack-plugin:

const InjectAssetsWebpackPlugin = require('inject-assets-webpack-plugin');
const webpackConfig = {
  ...
  plugins: [
    ...
    new InjectAssetsWebpackPlugin({
      filename: 'public/worker.js',
    },[
      { pattern: '{hash}', type: 'hash' },
      { pattern: '{hostname}', type: 'value', value: config.app.hostname },
      { pattern: '{api_hostname}', type: 'value', value: config.app.serverUri },
      {
        pattern: '{files_to_cache}',
        type: 'chunks',
        chunks: ['app', 'vendor', 'meta', 'AppBarContent'],
        files: ['.js', '.css'],
        excludeFiles: ['.map'],
        decorator: fileNames => fileNames.join('\', \''),
      },
    ]),
    ...
  ],
  ...
};

Заключение

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

Надеюсь, эта статья поможет кому-то сэкономить часы на поиск правильного способа создания прогрессивного веб-приложения. Исходный код полного примера доступен на моем GitHub. Рабочий пример можно найти на altbit-dev.co.uk.

Благодарю за вашу поддержку. Не стесняйтесь писать мне в любое время.