📜 ⬆️ ⬇️

Does your AngularJS work on 3.5Mb of RAM?


In early spring, ABBYY LS together with Xerox launched a service for translating documents Xerox Easy Translator Service. The highlight of this service is an application that runs on the Xerox MFP and allows you to scan the required number of documents, wait for the translation into one of the 38 languages ​​selected, print the translation - and all this without departing from the MFP.

The application runs on a specific series of Xerox MFPs based on the Xerox ConnectKey technology with a 800x480 pixel touch screen. The hardware of the MFP depends on the specific model, for example, our test little Xerox WorkCentre 3655 has a 1Ghz Dual Core processor and 2Gb of RAM on board. Surprisingly, the MFP has a built-in webkit browser, and our application is a regular html application developed on AngularJS 1.3.15.

We wrote about the project itself in the blog earlier, and this article is devoted to one of the exciting stages of the project, namely the optimization of AngularJS for work on the Xerox MFP. As it turned out, the MFP platform practically does not impose any serious restrictions on the development of applications, and they work almost the same way as on desktop webkit browsers, except for one BUT - The html application for JS execution will be allocated only 3.5 Mb of RAM (at this point, Xerox has already released an update for its platform, raising the threshold of allocated memory to 10 Mb). AngularJS ate these 3.5 Mb in a few minutes of work in the application, and the garbage collector of the built-in MFP browser did not have time for such voracity and simply knocked out our application on the main screen of the MFP. In addition, Xerox has no tools for analyzing and debugging applications running on the multifunction printer.
')
At first it seemed that it would not be possible to do anything (especially not having the knowledge of the voracity of modern browsers), but having correctly assessed the situation, we still decided to try to tame AngularJS and make the application consume the minimum possible amount of memory. Starting with 220kb compiled (minimized, not gzip) JS application code, we finished 97kb (AngularJS takes 56kb, all the rest is our code), removing all the unused code to the maximum, or modifying it for the least memory consumption. The result is a stable operation of the application for several tens of minutes on a platform with 3.5 Mb of memory and complete non-killability on a new platform from 10 Mb. What have we done?

Http Requests


The main problem we encountered right away is “heavy” http requests. Their “severity” is not measured in quantities or volumes of transmitted data, but in the new XmlHttpRequest object created at every request under the hood of $ http of the AngularJS service. The official information in the recommendations section of the Xerox SDK indicated that it is highly desirable to use only one XmlHttpRequest object in the application and to perform all requests sequentially using only one object.

Examples from the SDK were very simple - literally a couple of requests for the entire application, which, in principle, does not complicate the use of a single XmlHttpRequest object in its bare form using the native callback of this object. In our application, a very tricky logic of synchronizing user orders, oauth authorization, requests for MFP soap-services for launching scanning or printing is organized. In addition, requests to the MFP were performed using code from the Xerox SDK, which created its XmlHttpRequest object, pulled methods for working with the xml response of soap services, and generally created additional complexity when parsing this xml response and resulted in writing non-angular-way code.

Thus, we are faced with really serious problems: the lack of normal examples of real use of a single XmlHttpRequest object, a wide range of query usage, and semi-legacy code from the SDK. Despite the complexity, the way out was simple - write your $ http service, discard the code from the Xerox SDK and write your Angular services to support scanning and printing.

One of the main difficulties was also that our custom service should have the same software interface as the Angular $ http service in order to keep the already working and tested code of our controllers and $ http dependent services. Since only get and post requests were used in the application, in the simple annotation $ http.get (...) and $ http.post (...), the service itself looks like this:

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); } }; } 


This is the minimal type of our service, which, using one XmlHttpRequest object, is able to perform any number of http requests in succession without the threat of aggressive MFP memory consumption. In the end result, this service contains http interceptor functionality (without the ability to make changes to the final response of the request, it would be better to call http listeners, use for error logging), cancel the request queue $ http.cancel (), plus additional properties of the resulting object, which allow you to understand that the request was canceled by the user or fell off by timeout (30 seconds per request), for example:

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


The next step is to wrap the calls of the MFP soap services into the corresponding Angular services. The main problem here is that we get the answer from the multifunctional device in the form of cumbersome soap xml, and the actual data required is only a few bytes. To simplify this stage, from the source xml (which came to us as a string), we use the regular expression to “pull out” only the tag that interests us:

 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; } 


As a result, we get a DOM-tree, taking data from which is no longer difficult. In addition, the DOM tree can be used to search for tags of interest using the capabilities of the querySelector. Initially, the Xerox SDK code always parsed the entire xml response, and the DOM search was performed by custom traversing the tree to find the desired element (something like a custom XPath in JS). It's really hard to answer which of the approaches consumes memory and system resources better and less, but for some reason we personally trust the native functions of the DomParser.parseFromString browser, querySelector (querySelectorAll) to work with the DOM tree, rather than manually traversing.

Total:
It has developed its own functionality for executing http-requests and simple parsing of xml, in a reduced form, occupying 2.3kb. The entire dependent Xerox SDK code, which occupied 17kb in the minified form, was deleted from the application.
The $ http and $ httpBackend services have been removed from AngularJS.

Routing


Initially, the project used the well - known ui-router version 0.2.13. This is truly a wonderful, versatile and unique solution for AngularJS. Using it, we did quite normal application routing, nested states were used for modal windows.

Of course, there is a less functional and lightweight solution directly from the AngularJS developers themselves, which initially did not fit in its pure form and required improvements for modal windows. But it was the source code of this module that was actively used to develop its own solution. In the process of optimizing the application, we found that we did not need all the functionality of the ui-router module, namely, we did not need url-routing (the application on the multifunction device opens to full screen and no access to the address bar), nested states, resolve, etc. All that we need from the routing is:

1. The ability to easily configure the states (screens and modal windows) of an application.
2. Related directives and services for caching and navigation between screens and (or) modal windows.
3. Correct substitution and deletion of the visited screens html templates from the DOM tree, as well as the display of modal windows on top of the original screen (similar to the nested states of the ui-router, but we need only one nesting level).

The first point is implemented very easily:

 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 = [...]; }); 


At the configuration stage:

 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"); }]); 


The second item is implemented through services:

$ 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; } }; }]); 


and $ 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; }]; 


And also xrx-back directives:

 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); }); } } }]); 


and scriptDirective (for text / ng-template caching):

 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); } } }; }]); 


In the $ route service, we will organize additional logic for modal windows, namely: 1) we do not put them in the history of states and 2) when we try to call $ route.back when the modal window is open, we trigger a trigger event that we need to close the modal window. The xrx-view directive is signed for the event, which implements clause 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(); }); } }; }]); 


That's all. Routing is as lightweight as possible, supports work with both real html-templates and their counterparts from <script type = text / ng-template> ... </ script>, it implements the logic of modal windows we need. Additionally, it has a syntax similar to ui-router for working and configuring application states.

Total:
The ui-router with the size of 28kb was excluded from the application and its own functionality was developed in a minimal form occupying only 1.8kb.

The following services and directives have been removed from AngularJS:


Localization of the application


By the time we started full-scale application optimization, we already had an almost completely localized application in six languages ​​— English, German, French, Italian, Spanish, and Portuguese. Texts of languages ​​were stored by type key-value in JSON, and were substituted in the application using one-way binding {{:: locale.HELLO_HABR}}. In loading localization from JSON, everything is quite simple and there is nothing more to optimize:

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


Inside the $ locale function, the interface language is defined and the most suitable language is loaded from JSON using the global xhr.

But here the stage of real-time localization of the application can and should be optimized, although it uses one-sided binding, but still this is additional work within the digest cycle each time the page is accessed. In addition, in the localization there are texts with layout that require the use of ng-bind-html, and that in turn also entails additional checks by the $ sanitize service. The solution is far from the best, but actually it was practically impossible to do anything more convenient until such time as your routing was developed. With the advent of its own service for loading and caching html-templates $ view, of course, the idea came to use it to localize the application.

What did we have to do for this? In principle, quite a bit:

1. In all html templates, the places requiring localization, wrapped with double square brackets, a la, was {{:: locale.HELLO_HABR}}, it became - [[HELLO_HABR]]
2. Since such a combination of square brackets in the application is unique, we can make a regular replace using a regular expression, bypassing the stage of the finished DOM and the whole digest cycle, or more precisely, localizing before the template is compiled and inserted into 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); } }; }]); 


Thus, localization is triggered once at the time of the start of the Angular application, and we already store localized html templates in memory.

Total:
Localization of the application is removed from the digest cycle to the stage of application loading.
The following services and directives have been removed from AngularJS:


ng-model


The ng-model directive (and the other directives for working with html forms associated with it) is one of the pearls of AngularJS, it’s an incredible tool that you fall in love with from your first acquaintance. But few know what is hidden under the hood of the ng-model. This is actually a very heavy code that tracks events on the element (cut, paste, change, keydown), synchronizes the real value of the model with the displayed value on the screen, checks our model for each change, providing an interface controller for working with the model in our directives.

In fact, it turned out that we do not need all these opportunities. For example, validation is not needed, since even the authorization form, according to all guidelines, displays an error in a modal window only after an unsuccessful server request. Custom checkboxes, select boxes and lists also do not require verification, and the directives that implement them work with the model in read-write-watch mode. That is, the checkbox directive looks something like this:

 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"); }); } }; }); 


The only thing is that we have an authorization form on which we use text inputs. Therefore, the keyboard directive, like the ng-model, tracks cut, change, paste events, but in a more lightweight form, without starting the validation flywheel and other AngularJS goodies when working with models.

A few words about the keyboard, once it has been touched, here is its real look:


The entire layout is built on the JS side (the directive does not have an html template), from interesting optimizations - the click event is hung on a common container, and not on each button. This achieves a small, but still savings on event handlers, and consequently, the memory occupied by the application.

Total:
The following directives have been removed from 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

Scroll


A lot of fuel added to the fire to us and scrolling lists:



Optimizing the memory consumption, we abandoned ng-repeat (which creates its scope for each element), wrote our lightweight decision and thought that was all, but rendering the list of 38 languages ​​pretty much slowed down on the MFP. In addition, it also worsened that the MFP does not draw a system scroll in the browser and has to draw it with its own means. We tried many tricks, from using -webkit-scrollbar and ending with custom scrolling through element.scrollTop or -webkit-transform: translate (x, y) using overflow: hidden. Attempts to understand the principle of rendering the browser also failed. Either the scrolling itself slowed down, or the list was rebuilt (the user chose a different Source language and needed to rebuild the Target language list, which does not contain the selected Source language in itself).
Having almost lost hope, in one of the next experiments, we noticed that if you insert several elements into the list and change only their innerHTML, the rendering does not slow down, and the scrolling is carried out smoothly and without delays. In this difficult way, the directive for scrolling appeared in the application, the principle of its operation is simple and tricky at the same time:

1. The required number of elements is inserted into the container to fill its entire height, for example, 7 elements of the list.
2. Based on the offset value (indent from the beginning of the data array) and the html template, the innerHTML of our elements are changed.
3. We catch events of clicking on the scrolling arrows or “dragging” (mouseDown-mouseMove-mouseUp) of the slider, calculate the offset, change the position of the slider and return to step 2.
Thus, the sensation of data scrolling is created, although in reality only the internal contents of the same 7 elements of the list change.

Total:
The ng-repeat directive was removed from AngularJS, since there was no longer any sense in it, and all the work we needed was performed by a new scroll directive.

Additionally


Additionally, a number of shamanism was produced over AngularJS:

As a result, all directives were removed from the box from AngularJS, and the list of services acquired the following form:

We did not interfere with the initialization and the cycle of work of AngularJS, but slightly modified jqLite.

findings


Modern development of web applications comes by including rather large third-party solutions for the sake of one small function or feature, thereby increasing the gluttonousness of the application as if by leaps and bounds. Although, perhaps, this voracity is not so noticeable on desktops and laptops, devices with weak hardware stuffing such applications are digested with difficulty, additionally heating up considerably.

We learned from our experience that when the time comes for the necessary optimization, it is quite within its power to make almost any creative development team. The time spent on all the described optimizations was about 12-15% of the total project development time, which, in principle, is more than enough and we were very pleased with the results achieved.

AngularJS is truly a modular framework, and although it does not allow you to configure the required set of functions and download it (as you can do with jQuery UI, for example), we did not experience any discomfort when we exclude directives and services that we don’t need from AngularJS. It is a modular approach to application development that allows for virtually painless large-scale optimization and refactoring.

And yet I want to believe that such optimization is carried out not only forcedly, but also as part of caring for the end users of the system and the entire development team, and, perhaps, just on the wave of unrestrained enthusiasm.

Thanks to SimbirSoft specialists for their active participation in the work on the project.

Source: https://habr.com/ru/post/302960/


All Articles