В начале весны ABBYY LS совместно с Xerox запустили сервис для перевода документов Xerox Easy Translator Service. Изюминкой этого сервиса является приложение, запускаемое на МФУ Xerox и позволяющее отсканировать необходимое количество документов, дождаться перевода на один из выбранных 38 языков, произвести печать перевода — и все это не отходя от МФУ.

Приложение запускается на определенной серии МФУ Xerox на основе технологии Xerox ConnectKey с сенсорным экраном 800x480 точек. Аппаратная начинка МФУ зависит от конкретной модели, например, наша тестовая малютка Xerox WorkCentre 3655 имеет на борту 1Ghz Dual Core процессор и 2Gb оперативной памяти. Как ни удивительно, но МФУ имеет встроенный webkit-браузер, а наше приложение — это обычное html-приложение, разработанное на AngularJS 1.3.15.

О самом проекте мы писали в блоге раньше, а эта статья посвящена одному из увлекательных этапов проекта, а именно оптимизации AngularJS под работу на МФУ Xerox. Как оказалось на деле, платформа МФУ практически не накладывает никаких серьезных ограничений на разработку приложений, и они работают практически так же, как и на десктопных webkit-браузерах, за исключением одного НО — html-приложению для исполнения JS выделятся всего 3.5 Mb оперативной памяти (на данный момент Xerox уже выпустили обновление для своей платформы, подняв порог выделяемой памяти до 10 Mb). AngularJS эти 3.5 Mb съедал за несколько минут работы в приложении, а сборщик мусора встроенного браузера МФУ не успевал за такой прожорливостью и просто выбивал наше приложение на главный экран МФУ. Вдобавок у Xerox нет средств для анализа и дебага приложений, запущенных на МФУ.

Сначала казалось, что сделать ничего не получится (тем более непонаслышке зная прожорливость современных браузеров), но грамотно оценив ситуацию, мы все же решились попробовать укротить AngularJS и заставить приложение потреблять минимально возможное количество памяти. Начав с 220kb скомпилированного (минимизированного, не gzip) JS кода приложения, мы закончили 97kb (AngularJS занимает 56kb, все остальное – наш код), по максимуму удалив весь незадействованный код, либо видоизменив его для наименьшего потребления памяти. Результат – стабильная работа приложения в течение нескольких десятков минут на платформе с 3.5 Mb памяти и полная неубиваемость на новой платформе с 10 Mb. Что же мы сделали?

Http-запросы


Основная проблема, с которой мы столкнулись сразу же, — это “тяжелые” http-запросы. Их “тяжесть” измеряется не в количествах или объемах передаваемых данных, а в создаваемом при каждом запросе новом объекте XmlHttpRequest под капотом $http сервиса AngularJS. Официальная информация в секции рекомендаций SDK Xerox указывала на то, что крайне желательно использовать в приложении всего один объект XmlHttpRequest и все запросы последовательно выполнять, используя только один объект.

Примеры из SDK носили весьма простой характер — буквально пару запросов на все приложение, что, в принципе, никак не усложняет использование одного объекта XmlHttpRequest в голом виде с применением нативных коллбеков этого объекта. В нашем же приложении организована весьма хитрая логика синхронизации заказов пользователя, oauth авторизация, запросы к soap-сервисам МФУ для запуска сканирования или печати. К тому же, запросы к МФУ выполнялись с использованием кода из SDK Xerox, который создавал свой объект XmlHttpRequest, тянул за собой методы для работы с xml-ответом soap-сервисов и в целом создавал дополнительную сложность при парсинге этого xml-ответа и приводил к ситуации написания не Angular-way кода.

Таким образом, мы столкнулись с действительно серьезными проблемами: отсутствие нормальных примеров реального использования одного объекта XmlHttpRequest, широкого спектра использования запросов и полу-legacy кодом из SDK. Несмотря на всю сложность, выход из ситуации оказался прост – написать свой $http сервис, отказаться от кода из Xerox SDK и написать свои Angular-сервисы для поддержки сканирования и печати.

Одной из главных трудностей было еще и то, чтобы наш кастомный сервис должен иметь такой же программный интерфейс, как и ангуларовский $http сервис, чтобы сохранить уже работающий и протестированный код наших контроллеров и зависимых от $http сервисов. Так как в приложении использовались только get и post запросы, в простой аннотации $http.get(...) и $http.post(...), то сам сервис выглядит вот так:

function ($q) {

   var queue = [];

   // execute request
   function query() {
       var request = queue[0];
       var defer = request.defer;
       xhr.open(request.method, request.url, true);
       // set headers
       var headers = request.headers;
       for (var i in headers) {
           xhr.setRequestHeader(i, headers[i]);
       }
       // load callback
       xhr.onreadystatechange = function () {
           if (xhr.readyState == 4 && !defer.promise.$$state.status) {
               var status = xhr.status;
               var data = JSON.parse(xhr.response);
               (200 <= status && status < 300 ? defer.resolve : defer.reject)({
                   data: data,
                   status: status
               });
               queue.shift();
               if (queue.length) {
                   query();
               }
           }
       };
       // send data
       xhr.send(request.data);
   }

   // add request to queue
   function push(method, url, data, headers) {
       var defer = $q.defer();
       queue.push({
           data: typeof data === "string" ? data : JSON.stringify(data),
           defer: defer,
           headers: headers,
           method: method,
           url: url
       });
       if (queue.length == 1) query();
       return defer.promise;
   }

   return {
       // get request
       get: function (url, data, headers) {
           return push("GET", url, data, headers);
       },

       // post request
       post: function (url, data, headers) {
           return push("POST", url, data, headers);
       }
   };
}


Это минимальный вид нашего сервиса, который, используя один объект XmlHttpRequest, способен выполнять последовательно любое количество http-запросов без угрозы агрессивного потребления памяти МФУ. В конечном результате данный сервис содержит функциональность http interceptor’ов (без возможности внесения изменений в конечный ответ запроса, правильнее было бы назвать http listeners, используем для логирования ошибок), отмены очереди запросов $http.cancel(), плюс дополнительные свойства результирующего объекта, которые позволяют понять, что запрос был отменен пользователем или отвалился по таймауту (30 секунд на запрос), например:

$http.get(...).catch(function (response) { if (response.canceled) { ... } });


Следующий этап – обернуть вызовы soap-сервисов МФУ в соответствующие Angular-сервисы. Основная проблема здесь состоит в том, что ответ от МФУ мы получаем в виде громоздкой soap’овской xml, а реально необходимые данные занимают всего несколько байт. Чтобы упростить этот этап, из исходной xml (которая нам пришла в виде строки) мы с помощью регулярного выражения “вынимаем” только тот тег, который нам интересен:

var parser = new DOMParser();

function toXml (xml, tag) {
   if (tag) {
       var node = new RegExp('((<|&lt;)[\\w:]*' + tag + '(>|&gt;|\\s).*\/[\\w:]*' + tag + '(>|&gt;))', 'g').exec(xml);
       return node && node.length ? parse(node[1]) : null;
   } else {
       return parse(xml);
   }
}

function parse(xml) {
   return parser.parseFromString(xml
       .replace(/amp;/g, '')
       .replace(/</g, '<')
       .replace(/>/g, '>')
       .replace(/"/g, '"')
       .replace(/<\w+:/g, '<')
       .replace(/<\/\w+:/g, '<\/'), 'text/xml').documentElement;
}



В результате мы получаем DOM-дерево, забрать данные из которого уже не составляет труда. К тому же, по DOM-дереву можно искать интересующие нас теги, используя возможности querySelector. Изначально код из SDK Xerox всегда парсил xml ответ целиком, а поиск по DOM-дереву выполнялся путем кастомного обхода дерева до нахождения нужного элемента (что-то вроде куцего самописного XPath в JS). Действительно сложно ответить, какой из подходов лучше и меньше потребляет памяти и системных ресурсов, но лично мы почему-то больше доверяем нативным функциям браузера DomParser.parseFromString, querySelector (querySelectorAll) по работе с DOM деревом, чем ручной обход.

Итого:
Разработана своя функциональность для выполнения http-запросов и простого парсинга xml, в уменьшенном виде занимающие 2.3kb. Из приложения удален весь зависимый код SDK Xerox, в минифицированном виде занимавший 17kb.
Из AngularJS были удалены сервисы $http и $httpBackend.

Роутинг


Изначально в проекте использовался всем известный ui-router версии 0.2.13. Это действительно замечательное, разностороннее и уникальное в своем роде решение для AngularJS. Используя его, мы сделали вполне обычный роутинг приложения, для модальных окон использовались вложенные состояния.

Разумеется, есть менее функциональное и легковесное решение непосредственно от самих разработчиков AngularJS, которое изначально не подходило в своем чистом виде и требовало доработок для модальных окон. Но именно исходный код этого модуля был активно использован для разработки собственного решения. В процессе оптимизации приложения нами было обнаружено, что вся функциональность модуля ui-router нам не нужна, а именно – у нас не было необходимости в url-роутинге (приложение на МФУ открывается на весь экран и нет доступа к адресной строке), вложенных состояниях, resolve и пр. Все, что нам нужно от роутинга это:

1. Возможность простого конфигурирования состояний (экранов и модальных окон) приложения.
2. Сопутствующие директивы и сервисы для кэширования и навигации между экранами и (или) модальными окнами.
3. Корректная подстановка и удаление из DOM-дерева html-шаблонов посещаемых экранов, а также отображение модальных окон поверх исходного экрана (аналог вложенных состояний ui-router, но нам нужен всего один уровень вложенности).

Первый пункт реализуется весьма легко:

xerox.provider("$route", function () {
   ...
   var base = "/";
   var routes = {};
   var start;
   var self = this;

   // add new route
   function add(name, templateUrl, controller, modal) {
       routes[name] = {
           name: name,
           modal: modal,
           controller: controller,
           templateUrl: base + templateUrl + ".html"
       };
       return self;
   }

   // set start state
   self.start = function (name) {
      start = name;
      return self;
   };


   // add modal
   self.modal = function (name, templateUrl, controller) {
       return add(name, templateUrl, controller, true);
   };

   // add state
   self.state = function (name, templateUrl, controller) {
       return add(name, templateUrl, controller, false);
   };

   self.$get = [...];

});


На стадии конфигурирования:

xerox.config(["$routeProvider", function ($routeProvider) {

   $routeProvider
         
      // default state
      .start("settings")
          
      // modals
      .modal("login", "login/login", "login")
      .modal("logout", "login/logout", "logout")
      .modal("processing", "new-order/processing", "processing")
          
      // states
      .state("settings", "new-order/settings", "settings")
      .state("languages", "new-order/languages", "languages");

}]);


Второй пункт реализуется посредством сервисов:

$view

xerox.factory("$view", ["$http", "$locale", "$q", function ($http, $locale, $q) {

   var views = {};

   return {

       // get view
       get: function (url) {
           var self = this;
           if (views[url]) {
               return $q.when(views[url]);
           } else {
               return $http.get(url).then(function (response) {
                   var template = response.data;
                   self.put(url, template);
                   return template;
               });
           }
       },

       // put view
       put: function (url, text) {
           views[url] = text;
       }

   };
}]);


и $route

return {

   // route history
   var history = [];

   // $route interface
   var $route = {
       // current route
       current: null,

       // history back
       back: function () {
           if ($route.current.modal) {
               $rootScope.$broadcast("$routeClose");
           } else {
               $route.go(history.pop() && history.pop());
           }
       },

       // goto route
       go: function (name, params) {
           prepare(name, params);
       }
   };

   // prepare and load route
   function prepare(name, params) {
       var route = routes[name];
       $view.get(route.templateUrl).then(function (template) {
           route.template = template;
           commit(route, params);
       });
   }

   // commit route
   function commit(route, params) {
       route.params = params || {};
       if (!route.modal) {
           history.push(route.name);
       }
       $route.current = route;
       $rootScope.$broadcast("$routeChange");
   }

   // routing start
   prepare(start);

   return $route;
}];


А также директив xrx-back:

xerox.directive("xrxBack", ["$route", function ($route) {
   return {
       restrict: "A",
       link: function (scope, element) {
           element.on(xrxClick, $route.back);
       }
   };
}]);


xrx-sref:

xerox.directive("xrxSref", ["$route", function ($route) {
   return {
       restrict: "A",
       link: function (scope, element, attr) {
           element.on(xrxClick, function () {
               $route.go(attr.xrxSref);
           });
       }
   }
}]);


и scriptDirective (для кэширования text/ng-template):

xerox.directive("script", ["$view", function ($view) {
   return {
       restrict: "E",
       terminal: true,
       compile: function(element, attr) {
           if (attr.type == "text/ng-template") {
               $view.put(attr.id, element[0].text);
           }
       }
   };
}]);


В сервисе $route мы организуем дополнительную логику для модальных окон, а именно: 1) не помещаем их в историю состояний и 2) при попытке вызвать $route.back при открытом модальном окне триггерим событие, что необходимо закрыть модальное окно. На событие подписана директива xrx-view, которая и реализует пункт 3:

xerox.directive("xrxView", ["$compile", "$controller", "$route", function ($compile, $controller, $route) {
   return {
       restrict: "A",
       link: function (scope, element) {

           var stateScope;
           var modalScope;
           var modalElement;
           var targetElement;

           // destroy scope
           function $destroy(scope) { scope && scope.$destroy(); }

           // on route change
           scope.$on("$routeChange", function () {
               var current = $route.current;
               var newScope = scope.$new();

               // prepare scopes and DOM element
               $destroy(modalScope);
               if (current.modal) {
                   modalScope = newScope;
                   // find or create modal container
                   modalElement = element.find(".modals");
                   if (!modalElement.length) {
                       modalElement = xrxElement("<div class=modals>");
                       element.append(modalElement);
                   }
                   targetElement = modalElement;
               } else {
                   $destroy(modalScope);
                   $destroy(stateScope);
                   modalScope = null;
                   stateScope = newScope;
                   targetElement = element;
               }

               // append controller and inject { $scope, $routeParams }
               if (current.controller) {
                   targetElement.data("$ngControllerController", $controller(current.controller, {
                       $routeParams: current.params,
                       $scope: newScope
                   }));
               }

               // append Template to DOM and compile
               targetElement.html(current.template);
               $compile(targetElement.contents())(newScope);
           });

           // on modal close
           scope.$on("$routeClose", function () {
               $destroy(modalScope);
               modalScope = null;
               modalElement.remove();
           });

       }
   };
}]);


На этом всё. Роутинг является максимально легковесным, поддерживает работу как с реальными html-шаблонами, так и их аналогами из <script type=text/ng-template>...</script>, реализует необходимую нам логику модальных окон. Дополнительно имеет схожий с ui-router синтаксис для работы и конфигурирования состояний приложения.

Итого:
Из приложения был исключен ui-router размером 28kb и разработана своя функциональность в минимальном виде занимающий всего 1.8kb.

Из AngularJS были удалены следующие сервисы и директивы:
  • ng-controller
  • ng-include
  • scriptDirective (помещает в $templateCache скрипты c типом text/ng-template)
  • $anchorScroll
  • $location
  • $cacheFactory
  • $templateCache


Локализация приложения


К тому моменту, когда мы начали проводить полномасштабную оптимизацию приложения, мы уже имели почти полностью локализованное приложение на шести языках – английском, немецком, французском, итальянском, испанском и португальском. Тексты языков хранились по типу ключ-значение в JSON, а подставлялись в приложении с помощью одностороннего биндинга {{::locale.HELLO_HABR}}. В загрузке локализации из JSON все довольно просто и оптимизировать больше нечего:

angular.element(document).ready(function () {
   window.$locale(function () {
       angular.bootstrap(document.body, ["xerox"]);
   });
});


Внутри функции $locale идет определение языка интерфейса и подгружается наиболее подходящий язык из JSON с помощью глобального xhr.

Но вот стадию реалтаймовой локализации приложения можно и нужно оптимизировать, хотя она и использует односторонний биндинг, но все равно это дополнительная работа внутри digest цикла при каждом заходе на страницу. К тому же в локализации есть тексты с версткой, которые требуют применения ng-bind-html, а тот в свою очередь влечет и еще дополнительные проверки сервисом $sanitize. Решение далеко не из лучших, но ничего более удобного сделать, собственно, практически и нельзя было до того момента, пока не был разработан свой роутинг. С появлением собственного сервиса загрузки и кэширования html-шаблонов $view, несомненно, пришла идея использовать его для локализации приложения.

Что же нам для этого пришлось сделать? В принципе совсем немного:

1. Во всех html шаблонах места, требующие локализации, обернуть двойными квадратными скобками, а-ля, было {{::locale.HELLO_HABR}}, стало — [[HELLO_HABR]]
2. Так как такое сочетание квадратных скобок в приложении уникально, то мы можем с помощью регулярного выражения сделать обычный replace, минуя стадию готового DOM и в целом digest цикл, а если быть точнее – производить локализацию до того, как шаблон будет скомпилирован и вставлен в DOM:

2. xerox.factory("$view", ["$http", "$locale", "$q", function ($http, $locale, $q) {

   var views = {};
   // locale inject RegExp
   var localeRegExp = /\[\[(\w+)\]\]/mg;

   // template localization
   function localization(template) {
       var match;
       while (match = localeRegExp.exec(template)) {
           template = template.replace(match[0], $locale[match[1]]);
       }
       return template;
   }

   return {

       ...

       // put view
       put: function (url, text) {
           views[url] = localization(text);
       }
   };
}]);


Таким образом, локализация срабатывает однократно в момент старта Angular-приложения и в памяти мы уже храним локализованные html-шаблоны.

Итого:
Локализация приложения вынесена из цикла digest на стадию загрузки приложения.
Из AngularJS были удалены следующие сервисы и директивы:
  • ng-bind
  • ng-bind-html
  • ng-bind-template
  • ng-pluralize
  • $$sanitizeUri
  • $sce
  • $sceDelegate


ng-model


Директива ng-model (и остальные директивы для работы с html формами, связанные с ней,) – одна из жемчужин AngularJS, это невероятный инструмент, в который влюбляешься с первого знакомства. Но мало кто знает, что скрыто под капотом ng-model. Это в действительности весьма тяжеловесный код, отслеживающий события на элементе (cut, paste, change, keydown), синхронизирующий реальное значение модельки с отображаемым значением на экране, проверяющий при каждом изменении нашу модельку, предоставляющий интерфейс-контроллер для работы с моделькой в наших директивах.

На деле оказалось, что нам все эти возможности и не нужны. Например, не нужна валидация, так как даже форма авторизации, согласно всем гайдлайнам, выводит ошибку в модальном окне только после неудачного серверного запроса. Кастомные чекбоксы, селектбоксы и списки тоже не требуют проверки, а директивы, которые их реализуют, работают с моделькой в режиме read-write-watch. То есть директива checkbox выглядит как-то так:

xerox.directive("checkbox", function () {
   return {
       restrict: "E",
       scope: {
           xrxModel: "="
       },
       link: function (scope, element) {
           var icon = xrxElement("<div class=checkbox-icon>");
           element.prepend(icon);
           icon.on(xrxClick, function () {
               if (!element.attr(xrxDisabled)) {
                   scope.$apply(function () {
                       scope.xrxModel = !scope.xrxModel;
                   });
               }
           });
           scope.$watch("xrxModel", function (value) {
               element[value ? "addClass" : "removeClass"]("checked");
           });
       }
   };
});


Единственное – у нас есть форма авторизации, на которой мы используем текстовые инпуты. Поэтому директива клавиатуры, как и ng-model, отслеживает события cut, change, paste, но в более облегченном виде, не запуская маховик валидации и остальных вкусняшек AngularJS при работе с модельками.

Пару слов о клавиатуре, раз ее затронули, вот ее реальный вид:


Вся верстка строится на стороне JS (директива не имеет html шаблона), из интересных оптимизаций – событие клика навешивается на общий контейнер, а не на каждую кнопочку. Тем самым достигается небольшая, но все же экономия на обработчиках событий, а следовательно, и занимаемой приложением памяти.

Итого:
Из AngularJS были удалены следующие директивы:
ng-model
ng-list
ng-change
pattern
required
minlength
maxlength
ng-value
ng-model-options
ng-options
ng-init
ng-form
input
form
select

Скролл


Немало масла в огонь подлили нам и скроллируемые списки:



Оптимизируя потребление памяти, мы отказались от ng-repeat (который создает свой scope для каждого элемента), написали свое легковесное решение и думали, что на этом все, но вот рендеринг списка из 38 языков изрядно притормаживал на МФУ. Дополнительно дело ухудшало еще и то, что МФУ не рисует в браузере системный скролл и приходится его рисовать собственными средствами. Мы перепробовали множество ухищрений, начиная от использования -webkit-scrollbar и заканчивая кастомным скроллингом через element.scrollTop или -webkit-transform: translate(x, y) с использованием overflow: hidden. Попытки понять принцип рендеринга браузера тоже не увенчались успехом. Либо тормозил сам скролл, либо перестроение списка (пользователь выбрал другой Source язык и нужно перестроить список Target языков, который в себе не содержит выбранный Source язык).
Уже практически потеряв надежду, в один из очередных экспериментов, мы заметили, что если в список вставить несколько элементов и менять только их innerHTML, рендеринг не тормозит, а скролл осуществляется плавно и без задержек. Этим нелегким путем в приложении появилась директива для скролла, принцип ее работы прост и хитер одновременно:

1. В контейнер вставляется необходимое количество элементов, чтобы заполнить всю его высоту, например, 7 элементов списка.
2. На основе значения offset'а (отступ от начала массива данных) и html шаблона меняются innerHTML наших элементов.
3. Отлавливаем события нажатия на стрелочки скролла или «таскание» (mouseDown-mouseMove-mouseUp) ползунка, высчитываем offset, меняем позицию ползунка и возвращаемся к пункту 2.
Таким образом, создается ощущение скролла данных, хотя на деле меняется только внутреннее содержимое всех тех же 7 элементов списка.

Итого:
Из AngularJS была удалена директива ng-repeat, так как в ней больше не было ни какого смысла, а всю нужную нам работу выполняла новая директива скролла.

Дополнительно


Дополнительно над AngularJS был произведен еще ряд шаманств:
  • полностью удален функционал анимации ($animate, $$rAF);
  • удалены директивы для работы с классами и атрибутами (ng-class, ng-class-even и пр.);
  • ng-if и ng-switch заменены их оптимизированными аналогами – устанавливается display: none для html-элементов, не удовлетворяющих условию, минуя создание своего scope, как в оригинальных директивах, а ng-show и ng-hide удалены совсем;
  • удалены $filter и $locale, а все необходимые данные подготавливаются непосредственно в наших сервисах на основе небольших самописных решений.

В итоге из AngularJS были удалены все директивы из коробки, а список сервисов приобрел следующий вид:
  • $compile
  • $browser
  • $controller
  • $exceptionHandler
  • $interpolate
  • $log
  • $parse
  • $rootScope
  • $q

Мы не вмешивались в инициализацию и цикл работы AngularJS, но немного доработали jqLite.

Выводы


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

Мы на своем опыте убедились, что, когда наступает время необходимой оптимизации, её вполне по силам сделать практически любой творческой команде разработчиков. Затраченное время на все описанные оптимизации составило порядка 12-15% от общего времени разработки проекта, что, в принципе, более чем достаточно и мы остались очень довольны достигнутыми результатами.

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

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

Спасибо специалистам СимбирСофт за активное участие в работе над проектом.
Поделиться с друзьями
-->

Комментарии (24)


  1. Antelle
    22.06.2016 00:19

    На этой платформе каждое приложение должно сделать свою клавиатуру? Интересно, почему так.


    1. zatey
      22.06.2016 12:58

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


  1. alek0585
    22.06.2016 00:26
    +6

    Проще указать что же осталось в итоге от ангуляра. Кажется, что только сама структура: возможность написания сервисов, директив и контроллеров.
    Было бы интересно почитать о том, как происходил выбор либ и прочего.
    Статья супер!


    1. zatey
      22.06.2016 10:48

      Спасибо за наводку, добавили в секцию «Дополнительно» информацию о том, что же осталось от AngualrJS.


      1. alek0585
        23.06.2016 03:25
        +1

        Во, теперь вообще красота! Осталось выложить на гитхаб этот abbyLightAngular и наслаждаться)


  1. nazarpc
    22.06.2016 00:38
    +1

    Можно уточнить смысл (<|<), (>|>|\\s) и (>|>) в регулярках? Выглядит как бесполезное усложнение.


    1. Temtaime
      22.06.2016 02:21

      Первый и последний — это же смайлики!


    1. zatey
      22.06.2016 09:18

      Спасибо за замечание, исправили.
      Выглядит так:

      var node = new RegExp('((<|&lt;)[\\w:]*' + tag + '(>|&gt;|\\s).*\/[\\w:]*' + tag + '(>|&gt;))', 'g').exec(xml);
      

      Некоторые методы soap-сервиса МФУ возвращают в ответ xml со вложенной экранированной xml.


  1. napa3um
    22.06.2016 00:42
    +10

    Не рассматривали angularlight? Ну или knockout, vue или что-то ещё компактное и обеспечивающее только биндинги с DOM (остальное вы всё равно отрезали от оригинального ангуляра, насколько я понял).


    1. zatey
      22.06.2016 09:59
      +1

      Разумеется рассматривали и Angular Light и Vue.js и даже были маниакальные идеи переписать все на чистый JS.
      Но отправной точкой послужило почти готовое и протестированное приложение на AngularJS.
      С точки зрения рисков, мы оценили, что переписать приложение на другой JS фреймворк против доработать (зарефачить) готовую кодовую базу значительно проигрывает по трудозатратам.
      В любом случае, код для http-запросов, роутингу, скролу и т.д. пришлось бы писать свой, а «кастрация» AngualrJS до состояния более легковесных фреймворков занимает, уж поверьте, очень мало времени.
      Если же за отправную точку взять старт проекта, то тут разумеется выбор бы пал на Vue.js, сообщество Angular Light показалось нам каким-то полумертвым, а готовых модулей для http-запросов и роутинга днем с огнем не сыщешь.


  1. youROCK
    22.06.2016 01:09
    +2

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


    1. zatey
      22.06.2016 10:28

      Вы несомненно правы, но в нашем случае описанная оптимизация действительно заняла 12-15% от всего времени разработки проекта. А это не может не радовать как глаз, так и бюджет проекта.


  1. Sayonji
    22.06.2016 02:21
    +4

    Делать пул xhr объектов целесообразно сейчас для обычных браузеров? Пусть, например, веб-сайт собирается посылать по запросу раз в 10 секунд и работать в таком режиме пару дней на компьютере. Если реализовать переиспользование xhr, получится ли выигрыш по памяти, занимаемой вкладкой? Или gc всё сделает хорошо, и приём в статье нужен чисто потому, что оперативки ксерокса хватаешь лишь на считанные минуты, и gc не успевает всё подмести?


    1. zatey
      22.06.2016 10:20
      +2

      В обычной веб-разработке целесообразность не то чтобы нулевая, а скорее отрицательная, так как все параллельные запросы выстроятся в очередь и приложение станет менее отзывчивым. Про потребление памяти XHR тоже не беспокойтесь, а вот контент оптимизируйте.
      Если же ваше приложение работает на специфичной платформе с ограниченным количеством ОЗУ, то тут вам повезло — мы собрали грабли и поделились кодом.


      1. Sayonji
        22.06.2016 12:08
        +1

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


  1. DeLuxis
    22.06.2016 08:30

    Не думали переписать приложение на 1.5.*?


    1. Dub4ek
      22.06.2016 12:20

      Не понимаю, как использование AngularJs 1.5 вместо 1.3 поможет уменьшить вес приложения? Тут просто надо было выкинуть angular и взять KnockoutJs.


  1. vintage
    22.06.2016 09:20
    +1

    Что у вас вообще осталось от ангуляра-то? :-)


    1. xakboard
      22.06.2016 12:20
      +2

      Из AngularJS был удалён ангуляр. )


  1. Pinsky
    22.06.2016 10:14

    Мне кажется, что в вашем случае preact(preactjs.com) был бы адекватнее.


  1. Radjah
    22.06.2016 16:31
    +2

    > из исходной xml (которая нам пришла в виде строки) мы с помощью регулярного выражения “вынимаем” только тот тег, который нам интересен
    А это не является грязным хаком?


    1. zatey
      22.06.2016 19:25

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


      1. Radjah
        22.06.2016 23:21

        Я к тому, что если в следующей версии прошивки устройства производитель что-то там подправит, то есть ненулевая вероятность, что приложение может отвалиться. Или такова плата за уменьшение потребления памяти?


        1. zatey
          23.06.2016 06:35

          В таком случае отвалится не только этот хак, но и сам обход xml-дерева до нужной ноды, так как имя самой или одной из родительских нод может измениться.
          Ребята из Xerox версифицируют свои soap-сервисы, аля /v0/print или /v1/scan, все изменения новой платформы не затрагивают старые методы.