あなたのAngularJSは3.5MbのRAMで動作しますか?



早春、 ABBYY LSは Xeroxとともに、文書翻訳サービスであるXerox Easy Translator Serviceを開始しました。 このサービスのハイライトは、Xerox MFPで実行されるアプリケーションです。必要な数のドキュメントをスキャンし、選択した38言語のいずれかへの翻訳を待機し、翻訳を印刷できます。



このアプリケーションは、800x480ピクセルのタッチスクリーンを備えたXerox ConnectKeyテクノロジーに基づく特定のXerox MFPシリーズで実行されます。 MFPのハードウェアは特定のモデルに依存します。たとえば、テスト用のXerox WorkCentre 3655には、1Ghzデュアルコアプロセッサと2GbのRAMが搭載されています。 驚いたことに、MFPにはWebkitブラウザーが組み込まれており、アプリケーションはAngularJS 1.3.15で開発された通常のhtmlアプリケーションです。



このプロジェクトについてはブログの前半で書きましたが、この記事はプロジェクトの魅力的な段階の1つ、つまりXerox MFPでの作業のためのAngularJSの最適化に当てられています。 実際に判明したように、MFPプラットフォームは実際にはアプリケーション開発に重大な制限を課すことはなく、1つの例外を除いてデスクトップWebkitブラウザーとほぼ同じように機能します。 JS実行用のhtmlアプリケーションには、3.5 MbのRAMのみが割り当てられます(現時点では、Xeroxはプラットフォームの更新を既にリリースしており、割り当てられたメモリのしきい値を10 Mbに引き上げています)。 AngularJSは、アプリケーションでの作業の数分でこれらの3.5 Mbを消費し、組み込みのMFPブラウザーのガベージコレクターはそのような大食いに追いつかず、単純にアプリケーションをMFPメイン画面にノックしました。 さらに、Xeroxには、MFPで実行されているアプリケーションを分析およびデバッグするためのツールがありません。



最初は何もできなかったように見えましたが(特に、最新のブラウザのボラティリティを直接知っている場合)、状況を正しく評価して、AngularJSを飼いならし、アプリケーションのメモリ消費を最小限に抑えることにしました。 220kbのコンパイル済み(gzipではなく最小化)JSアプリケーションコードから始めて、97kb(AngularJSは56kb、他はすべてコードです)を終了し、未使用のコードを可能な限り削除するか、メモリ消費を最小限に抑えるように変更しました。 その結果、3.5 Mbのメモリを搭載したプラットフォームで数十分間安定したアプリケーションが実現し、10 Mbを搭載した新しいプラットフォームで完全に破壊されません。 私たちは何をしましたか?



HTTPリクエスト



私たちがすぐに直面した主な問題は、「重い」httpリクエストでした。 それらの「重大度」は、送信されるデータの量や量ではなく、$ http AngularJSサービスのフードの下で各リクエストで作成される新しいXmlHttpRequestオブジェクトで測定されます。 Xerox SDKの推奨セクションの公式情報は、アプリケーションで1つのXmlHttpRequestオブジェクトのみを使用し、1つのオブジェクトのみを使用してすべてのリクエストを順番に実行することを強くお勧めします。



SDKの例は非常にシンプルでした。文字通り、アプリケーション全体に対する2、3のリクエストであり、原則として、このオブジェクトのネイティブコールバックを使用して、単一のXmlHttpRequestオブジェクトをそのままの形で使用することを複雑にしません。 このアプリケーションでは、ユーザーの注文を同期する非常に巧妙なロジックが編成されています。oauth承認、スキャンまたは印刷を開始するためのMFP soapサービスへの要求です。 さらに、MFPへの要求は、独自のXmlHttpRequestオブジェクトを作成し、SOAPサービスのxml応答を操作するメソッドをプルし、このxml応答を解析するときに一般に追加の複雑さを作成し、状況を引き起こしたXerox SDKのコードを使用して実行されましたAngular-wayコードを書いていません。



したがって、1つのXmlHttpRequestオブジェクトの実際の使用に関する通常の例の欠如、SDKからの広範なクエリの使用、および準レガシーコードのような、深刻な問題に直面しました。 すべての複雑さにもかかわらず、方法は簡単でした。$ httpサービスを作成し、Xerox SDKからコードを破棄し、スキャンと印刷をサポートするためにAngularサービスを作成します。



主な問題の1つは、カスタムサービスが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); } }; }
      
      







これはサービスの最小形式であり、1つのXmlHttpRequestオブジェクトを使用して、MFPのメモリを積極的に消費することなく、任意の数のhttp要求を連続して実行できます。 最終結果では、このサービスにはhttpインターセプターの機能が含まれており(リクエストの最終応答に変更を加える可能性がなく、httpリスナーを呼び出す方が正確です。エラーをログに記録するために使用します)、リクエストキュー$ http.cancel()をキャンセルし、結果のオブジェクトの追加のプロパティ、これにより、リクエストがユーザーによってキャンセルされたか、タイムアウト(リクエストごとに30秒)で落ちたことを理解できます。次に例を示します。



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







次のステップは、MFP soapサービスコールを対応するAngularサービスでラップすることです。 ここでの主な問題は、MFPからかさばるsoap'ml 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の機能を使用して、興味のあるタグを検索できます。 最初、Xerox SDKのコードは常にxml応答を全体として解析し、DOMツリー内の検索は、目的の要素が見つかるまで(JSの走り書きXPathのようなもの)ツリーのカスタムトラバーサルによって実行されました。 どちらのアプローチがメモリとシステムリソースをより良く、そしてより少なく消費するかを答えるのは本当に難しいですが、何らかの理由で、私たちは個人的なネイティブブラウザ機能DomParser.parseFromString、querySelector(querySelectorAll)を手動でトラバースするよりもDOMツリーを操作するために個人的に信頼しています。



合計:

http-requestsと単純なxmlの構文解析を実行するための独自の機能を開発し、2.3kbを占める縮小形式で作成しました。 依存するXerox SDKコードはすべてアプリケーションから削除され、縮小された形式で17 kbを占有しました。

サービス$ httpおよび$ httpBackendは、AngularJSから削除されました。



ルーティング



当初、プロジェクトではよく知られたui-routerバージョン0.2.13を使用しました。 これは本当に素晴らしい、多目的で、AngularJSにとって類のないソリューションの1つです。 これを使用して、アプリケーションの非常に通常のルーティングを作成しました。モーダルウィンドウでは、ネストされた状態が使用されました。



もちろん、AngularJS開発者自身から直接提供された、機能的で軽量なソリューションはありますが、当初は純粋な形式には適合せず、モーダルウィンドウの改善が必要でした。 しかし、独自のソリューションの開発に積極的に使用されたのは、このモジュールのソースコードでした。 アプリケーションを最適化する過程で、UIルーターモジュールのすべての機能が必要ではないことがわかりました。つまり、URLルーティング(MFP上のアプリケーションは全画面で開き、アドレスバーへのアクセスはありません)、ネスト状態、ルーティングなどから必要なのは次のとおりです。



1.アプリケーションの状態(画面とモーダルウィンドウ)を簡単に構成する機能。

2.画面と(または)モーダルウィンドウ間のキャッシングとナビゲーションのための関連するディレクティブとサービス。

3. DOMツリーからの訪問済み画面のhtmlテンプレートの正しい置換と削除、および元の画面(ui-routerのネスト状態の類似物。ただし、ネストレベルは1つだけ必要です)の上のモーダルウィンドウの表示。



最初の段落は非常に簡単に実装できます。



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







2番目の項目は、サービスを通じて実装されます。



$ビュー



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







および$ルート



 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(テキストのキャッシュ/ 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); } } }; }]);
      
      







$ルートサービスでは、モーダルウィンドウの追加のロジックを整理します。つまり、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ルーターに似た構文があります。



合計:

28kbのui-routerはアプリケーションから除外され、独自の機能は1.8kbしか占有しない最小限の形式で開発されました。



以下のサービスとディレクティブは、AngularJSから削除されました。





アプリケーションのローカライズ



本格的なアプリケーションの最適化を実施し始めた頃には、英語、ドイツ語、フランス語、イタリア語、スペイン語、ポルトガル語の6つの言語でほぼ完全にローカライズされたアプリケーションが既にありました。 言語テキストはJSONのキーと値のタイプごとに保存され、一方向バインディング{{:: locale.HELLO_HABR}}を使用してアプリケーションで置換されました。 JSONからローカライズをロードする場合、すべてが非常に単純であり、最適化するものはこれ以上ありません。



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







$ロケール関数内で、インターフェース言語が定義され、JSONから最も適切な言語がグローバルxhrを使用してロードされます。



ただし、ここではアプリケーションのリアルタイムローカリゼーションの段階を最適化できます。ただし、一方向のバインディングを使用しますが、これはページに入るたびにダイジェストサイクル内の追加作業です。 さらに、ローカライズには、ng-bind-htmlの使用を必要とするレイアウトのテキストがあります。これは、$ sanitizeサービスによる追加のチェックを伴います。 ソリューションは最善とはほど遠いですが、実際には、ルーティングが開発された瞬間まで、これ以上便利なことはできませんでした。 $ view htmlテンプレートをロードおよびキャッシュするための独自のサービスの出現により、アプリケーションをローカライズするためにそれを使用するという考えは間違いありません。



これのために何をしなければなりませんでしたか? 原則として、かなり:



1.すべてのhtmlテンプレートで、ローカライズが必要な場所は二重角括弧で囲まれている必要があります。{{:: locale.HELLO_HABR}}があり、それは[[HELLO_HABR]]になりました。

2.この角括弧の組み合わせはアプリケーション内で一意であるため、正規表現を使用して通常の置換を行い、完成したDOMの段階とダイジェストサイクル全体をバイパスするか、より正確には、テンプレートをコンパイルして挿入する前にローカライズできます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テンプレートがすでに保存されています。



合計:

アプリケーションのローカライズは、ダイジェストサイクルからアプリケーションロードの段階まで削除されます。

以下のサービスとディレクティブは、AngularJSから削除されました。





ng-model



ng-modelディレクティブ(およびそれに関連付けられたHTMLフォームを操作するためのその他のディレクティブ)は、AngularJSの真珠の1つであり、最初の知り合いから恋に落ちる信じられないほどのツールです。 しかし、ng-modelの裏に隠れているものを知っている人はほとんどいません。 これは実際には非常に重いコードで、要素のイベント(カット、貼り付け、変更、キーダウン)を監視し、モデルの実際の値を画面に表示される値と同期し、モデルを変更ごとにチェックし、ディレクティブでモデルを操作するためのインターフェイスコントローラーを提供します。



実際、これらすべての機会が必要なわけではないことがわかりました。 たとえば、すべてのガイドラインによると、承認フォームでも、サーバー要求が失敗した後にのみモーダルウィンドウにエラーが表示されるため、検証は必要ありません。 カスタムチェックボックス、選択ボックス、およびリストも検証を必要とせず、それらを実装するディレクティブは読み取り/書き込み-ウォッチモードのモデルで動作します。 つまり、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などのキーボードディレクティブは、モデルを操作するときに検証フライホイールやその他のAngularJSの機能を開始することなく、カット、変更、貼り付けイベントを追跡しますが、より軽量な形式です。



キーボードについて触れたので、実際の外観を次に示します。





レイアウト全体は、興味深い最適化により、JS側で構築されます(ディレクティブにhtmlテンプレートはありません)-クリックイベントは、すべてのボタンではなく、共通のコンテナーでハングします。 これにより、イベントハンドラーの節約は小さくなりますが、それでもアプリケーションが占有するメモリが節約されます。



合計:

以下のディレクティブは、AngularJSから削除されました。

ng-model

ng-list

ng-change

模様

必要な

最小長

最大長

ng-value

ng-model-options

ng-options

ng-init

ng-form

入力



選択する



スクロール



火とスクロール可能なリストに多くのオイルが追加されました:







メモリ消費を最適化するために、ng-repeat(各要素に独自のスコープを作成する)を放棄し、軽量ソリューションを作成してそれだけであると考えましたが、38言語のリストのレンダリングはMFPでかなり遅くなりました。 さらに、この問題は、MFPがブラウザーにシステムスクロールを描画せず、独自の手段を使用して描画する必要があるという事実によって悪化しました。 -webkit-scrollbarの使用から、element.scrollTopまたは-webkit-transform:translate(x、y)through overflow:hiddenのカスタムスクロールまで、多くのトリックを試しました。 ブラウザをレンダリングする原理を理解する試みも失敗しました。 スクロール自体が遅くなるか、リストが再構築されました(ユーザーが別のソース言語を選択し、選択したソース言語を含まない言語のターゲットリストを再構築する必要があります)。

すでにほとんど希望を失い、次の実験の1つで、リストに複数の要素を挿入してinnerHTMLのみを変更すると、レンダリングが遅くならず、スクロールがスムーズに遅延なく実行されることに気付きました。 この困難な方法で、スクロールのディレクティブがアプリケーションに現れました。その操作の原理はシンプルでありながらunningです:



1.必要な数の要素がコンテナに挿入されて、リストの7つの要素など、コンテナの高さ全体が埋められます。

2.オフセット値(データ配列の先頭からのインデント)とhtmlテンプレートに基づいて、要素のinnerHTMLが変更されます。

3.スクロール矢印をクリックするか、スライダーの「ドラッグ」(mouseDown-mouseMove-mouseUp)イベントをキャッチし、オフセットを計算し、スライダーの位置を変更して、手順2に戻ります。

したがって、実際にはすべての同じ7つのリストアイテムの内部コンテンツのみが変更されますが、データスクロールの感覚が生まれます。



合計:

ng-repeatディレクティブはもはや意味をなさないため、AngularJSから削除され、新しいscrollディレクティブは必要なすべての作業を実行しました。



オプショナル



さらに、AngularJSで他の多くのシャーマニズムが実行されました。



その結果、ボックスのすべてのディレクティブがAngularJSから削除され、サービスのリストは次の形式を取りました。



AngularJSの初期化と作業サイクルには干渉しませんでしたが、jqLit​​eをわずかに変更しました。



結論



Webアプリケーションの最新の開発では、1つの小さな機能または機能のために、かなり大規模なサードパーティソリューションを含めることにより、アプリケーションの大食いを飛躍的に増やしています。 おそらく、この大食いはデスクトップやラップトップではそれほど顕著ではありませんが、ハードウェアの詰め物が弱いデバイスはそのようなアプリケーションを消化するのが難しく、さらにかなりウォームアップします。



私たち自身の経験から、必要な最適化の時が来ると、ほとんどすべての創造的な開発チームがそれを行うことができることがわかりました。 説明したすべての最適化に費やされた時間は、プロジェクト開発時間全体の約12〜15%でした。これは、原則として十分であり、達成された結果に非常に満足しています。



AngularJSは実際にはモジュラーフレームワークであり、必要な機能セットを構成してダウンロードすることはできませんが(たとえば、jQuery UIを使用して実行できます)、AngularJSから不要なディレクティブとサービスを除外する際に不快感はありませんでした。 大規模な最適化とリファクタリングをほぼ無痛で実行できるようにするのは、アプリケーション開発のモジュール式アプローチです。



それでも、このような最適化は、システムのエンドユーザーと開発チーム全体を気遣うという枠組みだけでなく、恐らく無制限の熱意の結果としても強制されると信じたいと思っています。



プロジェクトに積極的に参加してくれたSimbirSoftの専門家に感謝します。



All Articles