Дели — сокращай, или как мы делали мобильный 2ГИС Онлайн



    Мобильный веб развивается семимильными шагами. На дворе 2017 год. Мобильный трафик превысил десктопный — больше половины всех страниц теперь открываются через телефоны или планшеты. В 2015 году Google объявил о предпочтении mobile-friendly сайтов при ранжировании выдачи, а в 2016 это сделал Яндекс. Юзеры проводят в интернете 60-70 часов в месяц с мобильных устройств и не готовы идти на компромисс и пользоваться неадаптивными сайтами. И 2ГИС — не исключение. За 2 года рост мобильного трафика 2ГИС Онлайн составил 74%, а месячная аудитория превысила 6 миллионов человек.


    17 апреля мы зарелизили новый мобильный онлайн («Монлайн») — одностраничное приложение, доступное по адресу m.2gis.ru. Приложение запущено в двух городах: Уфе и Новосибирске, а в ближайшее время планируется релиз на всю Россию.


    Мы знали, что при мобильной разработке столкнемся с тремя проблемами:


    Мобильный интернет
    Мобильный интернет (2G, 3G, 4G) или Wi-Fi медленнее и не такой стабильный, как кабельный.


    Мобильные устройства
    Проблема мобилок — слабый процессор. Это влияет на парсинг JS, рендеринг и анимации.


    Мобильные браузеры
    Некоторые популярные мобильные браузеры не поддерживают часть CSS-свойств или методов JS API. Иногда мы думали, что вернулись в темные времена разработки под IE8. Мобильный интернет и процессор не позволяли использовать полифилы, а значит, приходилось выкручиваться самостоятельно.


    Мы сохранили функциональность десктопной онлайн-версии и заставили Монлайн работать даже на убитом телефоне вашего дедушки под Пензой. О том, как мы это сделали, расскажем ниже.


    Дели — сокращай


    В мире мобильного веба JS-разработчик следует одному кредо: дели — сокращай. Конечный бандл мобильного приложения должен мало весить и быть разбит на куски.


    Выбор фреймворка и библиотек


    Главным и единственным фреймворком фронтенда является Preact. Это не опечатка. Preact — легковесная альтернатива React, использующая тот же API и работающая с Virtual DOM. Преимуществами фреймворка является небольшой вес (3 kB gzip против 45 kB у React) и более высокая скорость рендеринга. Благодаря использованию Preact вместо React размер нашего вендора сократился на 90%.


    Preact отличается от React. Например, у него отсутствуют propTypes, но эта проблема решилась введением статической типизации, о которой расскажем в п. 3. Подробно различия между фреймворками описаны в официальном репозитории на github.


    Кроме того, мы стараемся писать самостоятельно и не подключать сторонние полифилы и тяжелые библиотеки. Необходимые полифилы грузятся асинхронно через require.ensure и не попадают в бандл. Каждый полифил подключается только в зависимости от условий. Например, в случае с полифилом для Android Browser — мы сэкономили 5 kB кода в gzip.


    Ленивая загрузка


    image

    JS сформирован. Теперь пришло время его разбить. JS код делится на 3 группы:


    • вендор — сторонние библиотеки;
    • app-бандл, в котором хранится главный код приложения;
    • либы и полифилы, подключаемые в зависимости от условий, таких как версия браузера;

    Вендор минимизировали через выбор Preact, либы и полифилы подключаем асинхронно и по потребности. Остался последний герой. Гзипнутый app-бандл весом в 143 килобайта в gzip. Это роскошь для мобильной разработки. Так, если пользователь зашел в карточку организации, нет смысла моментально грузить код, отвечающий за рендеринг карточки метро или достопримечательности. Чтобы уменьшить размер app-бандла и доставлять клиенту как можно меньше кода, мы сделали ленивую загрузку.


    Требования к коду № 1 в Монлайне гласит: «Код должен быть максимально простым и не знать или делать лишнего». На этом правиле базируется структура UI. В проекте 11 контейнеров и 85 «глупых» компонентов. «Глупые» компоненты не знают о существовании друг друга. Контейнеры объединяют компоненты в структуры и передают данные через пропсы. Карточка организации или отзыва, выдача зданий — примеры контейнеров. 6 контейнеров из 11 не связаны друг с другом, что позволяет разбить app-бандл на… интрига… 6 дополнительных чанков. При восстановлении браузер загружает app-бандл и нужный чанк, а затем асинхронно подгружаются остальные JS-файлы, отвечающие за неактивные контейнеры. Это не блокирует работу приложения. После релиза ленивой загрузки вес app-бандла сократился на 38%.


    Хранение и нормализация данных на клиенте


    В мобильном вебе критически важно быстро отображать страницы. В Монлайне, как и у 99,9% SPA, часть информации в контейнерах пересекается. Возьмем выдачу фирм и карточку отдельной организации. В выдаче выводится заголовок, адрес, расписание и т. д. Та же информация отображается и в карточке фирмы. Такая информация не меняется, пока юзер пользуется приложением. Нет смысла ждать ответа от сервера, если пользователь уже просматривал эту информацию в прошлом, ведь ее можно хранить на клиенте в единственном экземпляре и показывать данные здесь и сейчас.


    Нормализация данных на клиенте — это хранение уникальных данных в стейте, переиспользуемых в контейнерах. Хранение данных на клиенте увеличивает скорость отображения контента, а нормализация убирает дубли и разделяет статические данные и данные, зависящие от контекста.


    Нормализация держит стейт в чистоте. Если статические данные просмотрены, то они сохраняются в стейте в отдельной таблице. Например, если пользователь открывает одну фирму несколько раз, то в стейте она хранится в одном экземпляре с доступом по id. И информация об этой фирме используется для любых нужд: в карточке самой организации, выдачах, отзывах, фотографиях и т. д. За форматирование данных отвечают селекторы контейнеров.


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


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


    Облегчаем себе разработку


    В создании Монлайна поучаствовали 34 разработчика. В мастер слили 2 000 коммитов, написали 77 000 строк кода и создали 1425 файлов. В проекте участвовали люди из других команд, ребята приходили на стажировки. Мы хотели ускорить процесс разработки, сделать код понятным и документированным. Поэтому решили отказаться от динамической типизации в JavaScript.


    Статическая типизация


    Клиентская часть приложения написана на TypeScript. Статическая типизация — главное преимущество TypeScript над JS. Она бьет по рукам в случае ошибок при компиляции, документирует код изнутри и облегчает рефакторинг и отладку.


    Для управления состоянием приложения в проекте используется Redux. Redux сочетается с TypeScript. Разработчик знает, что передается в payload или meta и, соответственно, что приходит в редьюсер. Например:


    export const setScrollTop = (payload: number) => ({
      type: APPCONTEXT_CHANGE_SCROLL_TOP,
      payload
    });
    
    export const setErrorToFrame = (errorCode: ErrorCodeType) => ({
      type: APPCONTEXT_SET_ERROR_TO_FRAME,
      payload: { errorCode }
    });

    TypeScript упрощает работу с редьюсерами (чистые функции, которые вычисляют новую версию стейта и возвращают ее). Например, редьюсер, отвечающий за обновления информации о текущем контексте приложения в стейте, обрабатывает 69 экшнов. На выходе получаем метод со свитчем в 100 строк кода, возвращающий новый стейт. Критически важно в таком большом полотне обрабатывать только нужные экшны.


    export default function (state: AppContext = defaultState, action: AppAction): AppContext {
      switch (action.type) {
        case APPCONTEXT_ADD_FRAME:
          return appAddFrame(state, action.payload);
        case APPCONTEXT_REMOVE_ACTIVE_FRAME:
          return appRemoveActiveFrame(state);
       ....
        case APPCONTEXT_HIDE_MENU:
          return { ...state, isSideMenuShown: false };
        default:
          return state;

    Избежать путаницы с набором экшнов в редьюсере и данными в payload или meta помогает Discriminated Unions. В коде выше видно, что аргумент action описан типом AppAction, который выглядит так:


    export type AppAction = AppAddFrameAction | AppRemoveActiveFrame | AppChangeFramePos | AppChangeMode | AppChangeLandscape…

    AppAction объединяет в себя 60 интерфейсов (appAddFrameAction, AppRemoveActiveFrame и т. д.). Каждый интерфейс описывает экшн. Тип (type) экшна — строковый литерал — это дискриминант. Он определяет наличие и содержание внутренностей объекта, таких как payload или meta.


    export interface AppAddFrameAction {
      type: 'APPCONTEXT_ADD_FRAME';
      payload: Frame;
    }
    
    export interface AppRemoveActiveFrame {
      type: 'APPCONTEXT_REMOVE_ACTIVE_FRAME';
    }
    
    export interface AppChangeFramePos {
      type: 'APPCONTEXT_CHANGE_FRAME_POS';
      payload: FramePos;
    }

    Так TypeScript понимает, что для экшна с дискриминантом 'APPCONTEXT_ADD_FRAME' нужно передать payload с интерфейсом Frame, а в случае 'APPCONTEXT_REMOVE_ACTIVE_FRAME' ничего передавать не нужно.


    Preact также сочетается с TypeScript. В Preact отсутствуют реактовские propTypes, решающие проблемы проверки типов у компонента. Но TypeScript восполняет потерю. Например, разработчик знает, что передается через пропсы компоненту и что хранится в стейте.


    export interface IconProps {
      icon: SVGIcon;
      width?: number;
      height?: number;
      color?: string;
      className?: string;
    }
    
    export class Icon extends React.PureComponent<IconProps, {}> {
      constructor(props: IconProps) {
        super(props);
      }
    
      public render() {
        const { color, icon } = this.props;
        const iconStyle = color ? { color: this.props.color } : undefined;
    
        return (
          <svg
            width={this.props.width || icon.width}
            height={this.props.height || icon.height}
            style={iconStyle}
            className={this.props.className}>
            <use xlinkHref={icon.id} />
          </svg>
        );
      }
    }

    TypeScript задокументирован и имеет песочницу. Разумеется, язык не всесилен, на июль 2017 года открыто 2200 issues. Продукт Microsoft не поддерживает часть нововведений в ES6. Но медленно и верно эти проблемы решаются в каждом новом релизе.


    Создание и тестирование верстки


    В мобильном онлайне — 85 «глупых» компонентов. «Глупые» компоненты — визуальные сущности, отвечающие за представление полученных данных. В первую очередь мы хотели разделить верстку и интеграцию этих компонентов в приложении. Это позволило бы ускорить code review и тестирование ресурсом разработчиков. Для достижения этих целей используется Makeup.

    Makeup — графический интерфейс для быстрого и комфортного ручного регрессионного тестирования верстки. Подробно почитать об инструменте можно здесь, а потрогать — здесь.


    С помощью Makeup верстка компонентов делается на отдельном хосте с замоканными данными без привязки к приложению. Это позволяет тестировать визуализацию компонента на уровне разработки, подгонять верстку под pixel perfect и уже позже заниматься интегрированием.


    Так в Makeup выглядят различия между тем, что нарисовал дизайнер, и тем, что сверстал разработчик.


    image

    А так мы проверяем «одиннадцатиклассницу» и видим, что ничего не поехало:


    image

    Заключение


    Подытожим. Мы проделали большую работу по уменьшению бандла и увеличению скорости загрузки страницы. Данные хранятся на клиенте. Вендор весит на 90% меньше, чем изначально мог бы. Бандл не включает лишних библиотек и полифилов, а нужные отдаются кусочками по потребности. TypeScript дает больше контроля над приложением, а Makeup упрощает работу с визуальной составляющей Монлайна.


    В планах разбить «жирные» модули в зависимости от контекста (например, карточку фирмы или геообъекта разделить на отдельные чанки) и попробовать разбить редьюсеры.


    Статья посвящена JS-коду. Но разработческое кредо «дели — сокращай» распространяется и на CSS-бандл. В будущем мы также планируем заняться и сплиттингом стилей.

    2ГИС
    138.16
    Карта города и справочник предприятий
    Share post

    Comments 31

      +2

      А зачем нужен m.2gis.ru, у вас же есть мобильные приложения?

        0
        Есть огромная армия тех, кто пользуется исключительно браузером (ага, и VK запускает тоже из браузера)
          +3
          Несколько миллионов человек заходят на мобильную онлайн версию. Кто-то переходит из поисковиков, кто-то по ссылке, кто-то просто привык работать через браузер или еще не знает о наличие приложения. Некоторые пользователи не устанавливают приложение, например, недостаточно места на телефоне и.т.д.
            0

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

              0
              Телефоны с 8Гб памяти, и андроид умудряющийся плохо работать даже при наличии MicroSD на таком телефоне.
                +1
                Мобильное приложение в «старом телефоне» (с 512МБ памяти) после того, как перестало быть «бета», вешается и падает (кроме того, что загружается неприличное количество времени). (Предыдущая версия работает нормально, главное — отключить автообновление, иначе при доступном вайфае приложение обновится, а старые карты открывать не будет.)
                В новом телефоне с 2ГБ памяти оно работает хотя бы терпимо (непонятность «нового» интерфейса — это другой вопрос).
                  +1
                  Мобильное приложение они уже успешно испортили: дикий объем, странный интерфейс, долгая загрузка и периодически отваливающиеся карты.
                    +1

                    С интерфейсом перемудрили да. Еще прям бесит что оно отзумливает каждый раз когда что-то ищешь.

                      +1
                      И выползающая панель, которая злостно закрывает треть экрана.
                      0

                      Да, приложение стало просто запредельно задумчивым

                    0
                    У меня к вам есть большущая просьба: 2Gis не умеет делать навигацию и любим не за это. Дайте возможность открывать локацию в навигаторе. Нашли организацию через 2Gis, ткнули «открыть» в навигаторе, и едем.

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

                        Эмм… в мобильном приложении есть навигация с перепрокладкой маршрута и голосовыми инструкциями.

                        0
                        Этим летом немного путешествовал: Пермь, Москва, Питер, Сочи.
                        2ГИС на телефоне очень помогал с достопримечательностями, общественным транспортом и где похавать).
                        Только с метро что-то не срослось (уже не помню, что именно), ставил Яметро.
                          +1
                          Прочитал название, подумал что до Индии добрались :)
                            0

                            Народ, может не в тему, а кто может подсказать сервис(ы), типа 2gis, но для Канады?

                              0
                              Google Maps?
                                0

                                Сравнивая Google Maps для моего города и 2Gis, второй явно выигывает. Для других регионов это не так?

                                0
                                Yelp вроде как.
                                0
                                если 143кб app bundle — это много,
                                то что делать с 10-15 тайтлами карты по 80кб, которые в один момнет грузятся и скорее всего на слабом GPRS забивают канал?
                                  0
                                  Вначале мы подругажаем легкие тайлы с невысоким качеством после чего инициализируем само приложение. После инициализации подгружаются тайлы в хорошем качестве.
                                    0

                                    del

                                    0
                                    А чем собираете, webpack'ом? Смотрели ли на Rollup, ведь с ним может компактнее получиться? Используете ли Babel или транспилируете сразу с помощью TypeScript?
                                      0
                                      Собираемся на Webpack 3. Раньше были на 2, потом обновились. На RollUp не смотрели. Транспилируемся с помощью TypeScript.
                                      –8
                                      Лучше бы вы убрали из приложения рекламу специализированных магазинов с алкогольной и табачной отравой, пивных, кальянных и прочих подобных притонов. Пусть они загнутся от отсутствия посетителей, всем станет только лучше. Или вам плевать, кто платит деньги?
                                      И ещё: правильно писать «рутер»! Что за безграмотные неучи у вас работают! Вот недавно такое обнаружил, полюбуйтесь:

                                      Вы не знаете, как пишется слово «воин»? А нормальные кавычки вам лень набирать?
                                      Если вы сами не хотите исправлять, то дайте мне права верховного редактора, чтобы я мог исправлять описание любых объектов. После отмены анонимных правок это единственный выбор.
                                        0
                                        Кстати, перед «перед» на картинке, вероятно, должна быть запятая.
                                        Если это не мемориал только тем, кто пал непосредственно перед стадионом.
                                        0

                                        Спасибо, отличная статья.


                                        1. При разработке используете реакт (ради его инструментов разработки)? Или сразу все на преакте? Используете ли preact-compat?
                                        2. Будете ли смотреть в сторону реакт файбера? Или преакт полностью устраивает?
                                        3. Смотрели ли vue.js? Или преакт, опять же, полностью устраивает?
                                        4. Пробовали ли mobx вместо редакса?
                                          +1
                                          Спасибо:) Извиняюсь за долгий ответ)
                                          1. Сразу все на преакте, да используем preact-compat.
                                          2. Уже смотрим в сторону fiber. Несмотря на то, что он больше весит, но его дифф алгоритм удачнее преактовского. Ждем релиз
                                          3. Vue не смотрели. У реакта была больше документация, поддержки и сообщество в начале создания приложения.
                                          4. Также не пробовали mobx. Стали использовать стандартный стек: react/redux.

                                        Only users with full accounts can post comments. Log in, please.