PoPを介してWordPressWebサイトにコード分割機能を追加する
公開: 2022-03-10速度は、今日のWebサイトの最優先事項の1つです。 ウェブサイトの読み込みを高速化する1つの方法は、コード分割です。アプリケーションをオンデマンドで読み込むことができるチャンクに分割します。必要なJavaScriptのみを読み込み、それ以外は何も読み込みません。 JavaScriptフレームワークに基づくWebサイトは、人気のあるJavaScriptバンドラーであるWebpackを介してコード分割をすぐに実装できます。 ただし、WordPress Webサイトの場合、それはそれほど簡単ではありません。 まず、WebpackはWordPressで動作するように意図的に構築されていないため、設定するにはかなりの回避策が必要になります。 第二に、WordPressにネイティブのオンデマンドアセットローディング機能を提供するツールは利用できないようです。
WordPressに適切なソリューションがないため、自分で作成したWordPressWebサイトを構築するためのオープンソースフレームワークであるPoP用に独自のバージョンのコード分割を実装することにしました。 PoPがインストールされたWordPressWebサイトには、ネイティブにコード分割機能があるため、Webpackやその他のバンドラーに依存する必要はありません。 この記事では、フレームワークのアーキテクチャの側面に基づいてどのような決定が行われたかを説明しながら、それがどのように行われるかを示します。 最後に、コード分割を使用した場合と使用しない場合のWebサイトのパフォーマンス、および外部バンドラーに対してカスタム実装を使用することのメリットとデメリットを分析します。 楽しんでいただければ幸いです!
戦略の定義
コード分割は、大きく次の2つのステップに分けることができます。
- ルートごとにロードする必要のあるアセットを計算し、
- これらのアセットをオンデマンドで動的にロードします。
最初のステップに取り組むには、アプリケーション内のすべてのアセットを含むアセット依存マップを作成する必要があります。 アセットはこのマップに再帰的に追加する必要があります—それ以上アセットが必要なくなるまで、依存関係の依存関係も追加する必要があります。 次に、ルートのエントリポイント(つまり、実行を開始するファイルまたはコード)から最後のレベルまで、アセット依存マップをトラバースすることで、特定のルートに必要なすべての依存関係を計算できます。
2番目のステップに取り組むために、サーバー側で要求されたURLに必要なアセットを計算し、アプリケーションがそれらをロードする必要がある応答で必要なアセットのリストを送信するか、直接HTTP /を送信します。 2応答と一緒にリソースをプッシュします。
ただし、これらのソリューションは最適ではありません。 最初のケースでは、アプリケーションは応答が返された後にすべてのアセットを要求する必要があるため、アセットをフェッチするための追加の一連のラウンドトリップリクエストがあり、すべてのアセットが読み込まれる前にビューを生成できなかったため、結果としてユーザーは待機する必要があります(この問題は、サービスワーカーを介してすべてのアセットを事前にキャッシュすることで軽減されるため、待機時間は短縮されますが、応答が戻った後にのみ発生するアセットの解析は避けられません)。 2番目のケースでは、同じアセットを繰り返しプッシュする可能性があります(Cookieを介して既にロードしたリソースを示すなどの追加のロジックを追加しない限り、これは実際に望ましくない複雑さを追加し、応答がキャッシュされないようにします)。 CDNからアセットを提供することはできません。
このため、このロジックをクライアント側で処理することにしました。 各ルートに必要なアセットのリストがクライアントのアプリケーションで利用できるようになるため、要求されたURLに必要なアセットがすでに認識されています。 これにより、上記の問題が解決されます。
- サーバーの応答を待たずに、アセットをすぐにロードできます。 (これをサービスワーカーと組み合わせると、応答が返されるまでに、すべてのリソースがロードおよび解析されているため、追加の待機時間はありません。)
- アプリケーションは、どのアセットがすでにロードされているかを認識しています。 したがって、そのルートに必要なすべてのアセットを要求するのではなく、まだロードされていないアセットのみを要求します。
このリストをフロントエンドに配信することのマイナス面は、Webサイトのサイズ(利用できるルートの数など)によっては、リストが重くなる可能性があることです。 アプリケーションの認識される読み込み時間を増やすことなく、それを読み込む方法を見つける必要があります。 これについては後で詳しく説明します。
これらの決定を行った後、アプリケーションでコード分割を設計して実装することができます。 理解を容易にするために、プロセスは次のステップに分割されています。
- アプリケーションのアーキテクチャを理解し、
- アセットの依存関係のマッピング、
- すべてのアプリケーションルートを一覧表示し、
- 各ルートに必要なアセットを定義するリストを生成し、
- アセットを動的にロードし、
- 最適化の適用。
さっそく始めましょう!
0.アプリケーションのアーキテクチャを理解する
すべてのアセットの関係を相互にマッピングする必要があります。 この目標を達成するための最適なソリューションを設計するために、PoPのアーキテクチャの特殊性を調べてみましょう。
PoPは、WordPressをラップするレイヤーであり、WordPressをアプリケーションを強化するCMSとして使用できるようにしますが、クライアント側でコンテンツをレンダリングして動的なWebサイトを構築するためのカスタムJavaScriptフレームワークを提供します。 これは、Webページの構築コンポーネントを再定義します。WordPressは現在、HTMLを生成する階層テンプレート( single.php
、 home.php
、 archive.php
など)の概念に基づいていますが、PoPは「モジュール、 」は、アトミック機能または他のモジュールの構成のいずれかです。 PoPアプリケーションの構築は、 LEGOで遊ぶことに似ています。つまり、モジュールを積み重ねたり、ラップしたりして、最終的にはより複雑な構造を作成します。 これは、Brad Frostのアトミックデザインの実装と考えることもでき、次のようになります。
モジュールは、ブロック、blockGroups、pageSections、topLevelsなどの上位エンティティにグループ化できます。 これらのエンティティもモジュールであり、追加のプロパティと責任があり、各モジュールがすべての内部モジュールのプロパティを表示および変更できる厳密なトップダウンアーキテクチャに従って相互に含まれています。 モジュール間の関係は次のようになります。
- 1つのtopLevelにはN個のpageSectionsが含まれます。
- 1 pageSectionには、N個のブロックまたはblockGroupsが含まれています。
- 1つのblockGroupには、N個のブロックまたはblockGroupが含まれます。
- 1ブロックにはN個のモジュールが含まれます。
- 1つのモジュールには、無限にN個のモジュールが含まれます。
PoPでJavaScriptコードを実行する
PoPは、pageSectionレベルから開始して、すべてのモジュールを繰り返し処理し、モジュールの事前定義されたハンドルバーテンプレートを介して各モジュールをレンダリングし、最後に、モジュールの対応する新しく作成された要素をDOMに追加することによってHTMLを動的に作成します。 これが行われると、モジュールごとに事前定義されたJavaScript関数が実行されます。
PoPは、アプリケーションのフローがクライアントで発生しないという点でJavaScriptフレームワーク(ReactやAngularJSなど)とは異なりますが、モジュールの構成(PHPオブジェクトでコード化されている)内のバックエンドで構成されます。 WordPressアクションフックの影響を受けて、PoPはパブリッシュ/サブスクライブパターンを実装します。
- 各モジュールは、対応する新しく作成されたDOM要素で実行する必要のあるJavaScript関数を定義しますが、このコードを実行するものやコードがどこから来るかを事前に知っている必要はありません。
- JavaScriptオブジェクトは、実装するJavaScript関数を登録する必要があります。
- 最後に、実行時に、PoPはどのJavaScriptオブジェクトがどのJavaScript関数を実行する必要があるかを計算し、それらを適切に呼び出します。
たとえば、対応するPHPオブジェクトを介して、calendarモジュールは、次のようにDOM要素でcalendar
関数を実行する必要があることを示します。
class CalendarModule { function get_jsmethods() { $methods = parent::get_jsmethods(); $this->add_jsmethod($methods, 'calendar'); return $methods; } ... }
次に、JavaScriptオブジェクト(この場合はpopFullCalendar
)は、 calendar
関数が実装されていることを通知します。 これは、 popJSLibraryManager.register
を呼び出すことによって行われます。
window.popFullCalendar = { calendar : function(elements) { ... } }; popJSLibraryManager.register(popFullCalendar, ['calendar', ...]);
最後に、 popJSLibraryManager
は、何がどのコードを実行するかについてマッチングを行います。 これにより、JavaScriptオブジェクトは、実装する関数を登録でき、サブスクライブされたすべてのJavaScriptオブジェクトから特定の関数を実行するメソッドを提供します。
window.popJSLibraryManager = { libraries: [], methods: {}, register : function(library, methods) { this.libraries.push(library); for (var i = 0; i < methods.length; i++) { var method = methods[i]; this.methods[method] = this.methods[method] || []; this.methods[method].push(library); } }, execute : function(method, elements) { var libraries = this.methods[method] || []; for (var i = 0; i < libraries.length; i++) { var library = libraries[i]; library[method](elements); } } }
新しいカレンダー要素がDOMに追加された後、そのIDはcalendar-293
になり、PoPは次の関数を実行するだけです。
popJSLibraryManager.execute("calendar", document.getElementById("calendar-293"));
エントリーポイント
PoPの場合、JavaScriptコードを実行するためのエントリポイントは、HTML出力の最後にある次の行です。
<script type="text/javascript">popManager.init();</script>
popManager.init()
は、最初にフロントエンドフレームワークを初期化し、次に、上記で説明したように、レンダリングされたすべてのモジュールによって定義されたJavaScript関数を実行します。 以下は、この関数の非常に単純化された形式です(元のコードはGitHubにあります)。 popJSLibraryManager.execute('pageSectionInitialized', pageSection)
およびpopJSLibraryManager.execute('documentInitialized')
を呼び出すことにより、これらの関数( pageSectionInitialized
およびdocumentInitialized
)を実装するすべてのJavaScriptオブジェクトがそれらを実行します。
(function($){ window.popManager = { // The configuration for all the modules (including pageSections and blocks) in the application configuration : {...}, init : function() { var that = this; $.each(this.configuration, function(pageSectionId, configuration) { // Obtain the pageSection element in the DOM from the ID var pageSection = $('#'+pageSectionId); // Run all required JavaScript methods on it this.runJSMethods(pageSection, configuration); // Trigger an event marking the block as initialized popJSLibraryManager.execute('pageSectionInitialized', pageSection); }); // Trigger an event marking the document as initialized popJSLibraryManager.execute('documentInitialized'); }, ... }; })(jQuery);
runJSMethods
関数は、最上位のモジュールであるpageSectionから開始し、そのすべての内部ブロックとその内部モジュールの行に沿って、すべてのモジュールに対して定義されたJavaScriptメソッドを実行します。
(function($){ window.popManager = { ... runJSMethods : function(pageSection, configuration) { // Initialize the heap with "modules", starting from the top one, and recursively iterate over its inner modules var heap = [pageSection.data('module')], i; while (heap.length > 0) { // Get the first element of the heap var module = heap.pop(); // The configuration for that module contains which JavaScript methods to execute, and which are the module's inner modules var moduleConfiguration = configuration[module]; // The list of all JavaScript functions that must be executed on the module's newly created DOM elements var jsMethods = moduleConfiguration['js-methods']; // Get all of the elements added to the DOM for that module, which have been stored in JavaScript object `popJSRuntimeManager` upon creation var elements = popJSRuntimeManager.getDOMElements(module); // Iterate through all of the JavaScript methods and execute them, passing the elements as argument for (i = 0; i < jsMethods.length; i++) { popJSLibraryManager.execute(jsMethods[i], elements); } // Finally, add the inner-modules to the heap heap = heap.concat(moduleConfiguration['inner-modules']); } }, }; })(jQuery);
要約すると、PoPでのJavaScriptの実行は、ゆるやかに結合されています。依存関係を固定する代わりに、JavaScriptオブジェクトがサブスクライブできるフックを介してJavaScript関数を実行します。
WebページとAPI
PoPWebサイトは自己消費型APIです。 PoPでは、WebページとAPIの区別はありません。各URLはデフォルトでWebページを返し、パラメータoutput=json
を追加するだけで、代わりにAPIを返します(たとえば、getpop.org / en /はWebページ、およびgetpop.org/en/?output=jsonがそのAPIです)。 APIは、PoPでコンテンツを動的にレンダリングするために使用されます。 したがって、別のページへのリンクをクリックすると、APIが要求されます。これは、その時点でWebサイトのフレーム(トップナビゲーションやサイドナビゲーションなど)が読み込まれるためです。その後、APIモードに必要なリソースのセットが読み込まれます。 Webページからのサブセットになります。 ルートの依存関係を計算するときは、これを考慮する必要があります。最初にWebサイトをロードするときにルートをロードするか、リンクをクリックして動的にロードすると、必要なアセットの異なるセットが生成されます。
これらは、コード分割の設計と実装を定義するPoPの最も重要な側面です。 次のステップに進みましょう。
1.アセット依存関係のマッピング
JavaScriptファイルごとに構成ファイルを追加して、それらの明示的な依存関係を詳しく説明することができます。 ただし、これはコードを複製し、一貫性を保つのが困難になります。 よりクリーンな解決策は、JavaScriptファイルを唯一の正しい情報源として保持し、JavaScriptファイル内からコードを抽出し、このコードを分析して依存関係を再作成することです。
マッピングを再作成できるようにするために、JavaScriptソースファイルで探しているメタデータは次のとおりです。
-
this.runJSMethods(...)
などの内部メソッド呼び出し; -
popJSRuntimeManager.getDOMElements(...)
などの外部メソッド呼び出し; -
popJSLibraryManager.execute(...)
のすべてのオカレンス。これは、JavaScript関数を実装するすべてのオブジェクトでJavaScript関数を実行します。 - どのJavaScriptオブジェクトがどのJavaScriptメソッドを実装するかを取得するための
popJSLibraryManager.register(...)
のすべての出現。
次のように、jParserとjTokenizerを使用して、PHPでJavaScriptソースファイルをトークン化し、メタデータを抽出します。
- 内部メソッド呼び出し(
this.runJSMethods
など)は、次のシーケンスを見つけるときに推定されます:トークンthis
またはthat
+.
+内部メソッド(runJSMethods
)の名前である他のトークン。 - 次のシーケンスを見つけると、外部メソッド呼び出し(
popJSRuntimeManager.getDOMElements
など)が推測されます。アプリケーション内のすべてのJavaScriptオブジェクトのリストに含まれるトークン(このリストは事前に必要です。この場合、オブジェクトpopJSRuntimeManager
が含まれます) +.
+外部メソッド(getDOMElements
)の名前である他のトークン。 -
popJSLibraryManager.execute("someFunctionName")
が見つかると、JavascriptメソッドがsomeFunctionName
であると推測されます。 -
popJSLibraryManager.register(someJSObject, ["someFunctionName1", "someFunctionName2"])
が見つかると、JavascriptオブジェクトsomeJSObject
を推測して、メソッドsomeFunctionName1
、someFunctionName2
を実装します。
スクリプトを実装しましたが、ここでは説明しません。 (長すぎるとあまり価値がありませんが、PoPのリポジトリにあります)。 このスクリプトは、Webサイトの開発サーバー(サービスワーカーに関する以前の記事で説明した方法論)の内部ページを要求すると実行され、マッピングファイルを生成してサーバーに保存します。 生成されたマッピングファイルの例を用意しました。 これは単純なJSONファイルであり、次の属性が含まれています。
-
internalMethodCalls
JavaScriptオブジェクトごとに、内部関数からの依存関係をリストします。 -
externalMethodCalls
JavaScriptオブジェクトごとに、内部関数から他のJavaScriptオブジェクトの関数への依存関係をリストします。 -
publicMethods
登録されているすべてのメソッドと、メソッドごとに、どのJavaScriptオブジェクトがそれを実装しているかを一覧表示します。 -
methodExecutions
JavaScriptオブジェクトごと、および内部関数ごとに、popJSLibraryManager.execute('someMethodName')
を介して実行されるすべてのメソッドを一覧表示します。
結果はまだアセット依存マップではなく、JavaScriptオブジェクト依存マップであることに注意してください。 このマップから、あるオブジェクトの関数が実行されるたびに、他のどのオブジェクトも必要になるかを確認できます。 すべてのアセットについて、各アセットに含まれるJavaScriptオブジェクトを構成する必要があります(jTokenizerスクリプトでは、JavaScriptオブジェクトは外部メソッド呼び出しを識別するために探しているトークンであるため、この情報はスクリプトへの入力であり、次のことができます。ソースファイル自体からは取得できません)。 これは、resourceloader-processor.phpなどのResourceLoaderProcessor
オブジェクトを介して行われます。
最後に、マップと構成を組み合わせることで、アプリケーション内のすべてのルートに必要なすべてのアセットを計算できるようになります。
2.すべてのアプリケーションルートの一覧表示
アプリケーションで使用可能なすべてのルートを識別する必要があります。 WordPress Webサイトの場合、このリストは各テンプレート階層のURLで始まります。 PoP用に実装されているものは次のとおりです。
- ホームページ:https://getpop.org/en/
- 著者:https://getpop.org/en/u/leo/
- シングル:https://getpop.org/en/blog/new-feature-code-splitting/
- タグ:https://getpop.org/en/tags/internet/
- ページ:https://getpop.org/en/philosophy/
- カテゴリ:https://getpop.org/en/blog/(カテゴリは実際にはページとして実装され、URLパスから
category/
を削除します) - 404:https://getpop.org/en/this-page-does-not-exist/
これらの階層ごとに、一意の構成を生成するすべてのルートを取得する必要があります(つまり、一意のアセットのセットが必要になります)。 PoPの場合、次のようになります。
- ホームページと404はユニークです。
- タグページは、どのタグに対しても常に同じ構成になります。 したがって、任意のタグの単一のURLで十分です。
- 単一の投稿は、投稿の種類(「イベント」や「投稿」など)と投稿のメインカテゴリ(「ブログ」や「記事」など)の組み合わせによって異なります。 次に、これらの組み合わせごとにURLが必要です。
- カテゴリページの構成は、カテゴリによって異なります。 したがって、すべての投稿カテゴリのURLが必要になります。
- 著者ページは、著者の役割(「個人」、「組織」、または「コミュニティ」)によって異なります。 したがって、それぞれがこれらの役割の1つを持つ3人の作成者のURLが必要になります。
- 各ページには独自の構成(「ログイン」、「お問い合わせ」、「私たちの使命」など)を設定できます。 したがって、すべてのページURLをリストに追加する必要があります。
ご覧のとおり、リストはすでにかなり長いです。 さらに、アプリケーションは、構成を変更するパラメーターをURLに追加する場合があり、必要なアセットも変更する可能性があります。 たとえば、PoPは、次のURLパラメータを追加することを提案します。
- tab(
?tab=…
)、関連情報を表示するには:https://getpop.org/en/blog/new-feature-code-splitting/?tab = authors; - format(
?format=…
)、データの表示方法を変更するには:https://getpop.org/en/blog/?format = list; - target(
?target=…
)、別のページでページを開くセクション:https://getpop.org/en/add-post/?target = addons。
初期ルートの中には、上記のパラメーターを1つ、2つ、または3つ持つことができ、さまざまな組み合わせを作成できます。
- 単一の投稿:https://getpop.org/en/blog/new-feature-code-splitting/
- 単一の投稿の作成者:https://getpop.org/en/blog/new-feature-code-splitting/?tab = authors
- リストとしての単一の投稿の作成者:https://getpop.org/en/blog/new-feature-code-splitting/?tab = authors&format = list
- モーダルウィンドウのリストとしての単一の投稿の作成者:https://getpop.org/en/blog/new-feature-code-splitting/?tab = authors&format = list&target = modals
要約すると、PoPの場合、可能なすべてのルートは次の項目の組み合わせです。
- すべての初期テンプレート階層ルート。
- 階層が異なる構成を生成するすべての異なる値。
- 各階層で使用可能なすべてのタブ(階層が異なればタブ値も異なる場合があります。1つの投稿に「作成者」と「応答」のタブを含めることができ、作成者に「投稿」と「フォロワー」のタブを含めることができます)。
- 各タブで可能なすべての形式(異なるタブには異なる形式が適用される場合があります。「作成者」タブには「マップ」形式がありますが、「応答」タブにはない場合があります)。
- 各ルートが表示される可能性のあるpageSectionsを示すすべての可能なターゲット(メインセクションまたはフローティングウィンドウで投稿を作成できますが、「Share with yourfriends」ページはモーダルウィンドウで開くように設定できます)。
したがって、少し複雑なアプリケーションの場合、すべてのルートを含むリストを手動で作成することはできません。 次に、データベースからこの情報を抽出して操作し、最後に必要な形式で出力するスクリプトを作成する必要があります。 このスクリプトは、すべての投稿カテゴリを取得し、そこからすべての異なるカテゴリページのURLのリストを作成します。次に、カテゴリごとに、データベースに同じ下の投稿をクエリします。これにより、単一のURLが作成されます。すべてのカテゴリの下に投稿するなど。 function get_resources()
から始まる完全なスクリプトが利用可能です。この関数は、各階層ケースによって実装されるフックを公開します。
3.各ルートに必要なアセットを定義するリストの生成
これで、アセット依存マップとアプリケーション内のすべてのルートのリストができました。 次に、これら2つを組み合わせて、ルートごとに必要なアセットを示すリストを作成します。
このリストを作成するには、次の手順を適用します。
- すべてのルートで実行されるすべてのJavaScriptメソッドを含むリストを作成します。
ルートのモジュールを計算し、各モジュールの構成を取得し、構成からモジュールが実行する必要のあるJavaScript関数を抽出し、それらをすべて足し合わせます。 - 次に、各JavaScript関数のアセット依存関係マップをトラバースし、必要なすべての依存関係のリストを収集して、それらをすべて一緒に追加します。
- 最後に、そのルート内の各モジュールをレンダリングするために必要なハンドルバーテンプレートを追加します。
さらに、前述のように、各URLにはWebページとAPIモードがあるため、上記の手順をモードごとに1回ずつ、2回実行する必要があります(つまり、APIモードのルートを表すパラメーターoutput=json
をURLに追加すると、 WebページモードではURLを変更しないでください)。 次に、用途の異なる2つのリストを作成します。
- Webページモードリストは、最初にWebサイトをロードするときに使用されるため、そのルートに対応するスクリプトが最初のHTML応答に含まれます。 このリストはサーバーに保存されます。
- APIモードリストは、Webサイトにページを動的にロードするときに使用されます。 このリストはクライアントにロードされ、リンクがクリックされたときにアプリケーションがオンデマンドでロードする必要のある追加のアセットを計算できるようにします。
ロジックの大部分は、 function add_resources_from_settingsprocessors($fetching_json, ...)
から実装されています(リポジトリにあります)。 パラメータ$fetching_json
は、Webページ( false
)モードとAPI( true
)モードを区別します。
Webページモードのスクリプトを実行すると、resourceloader-bundle-mapping.jsonが出力されます。これは、次のプロパティを持つJSONオブジェクトです。
-
bundle-ids
これは、バンドルIDでグループ化された、最大4つのリソース(実稼働環境用に名前が変更されています:eq
=>handlebars
、er
=>handlebars-helpers
など)のコレクションです。 -
bundlegroup-ids
これはbundle-ids
のコレクションです。 各bundleGroupは、一意のリソースのセットを表します。 -
key-ids
これは、ルート(ルートを一意にするすべての属性のセットを識別するハッシュで表される)と対応するbundleGroupの間のマッピングです。
観察できるように、ルートとそのリソースの間のマッピングはまっすぐではありません。 key-ids
をリソースのリストにマッピングする代わりに、キーIDを一意のbundleGroupにマッピングします。これは、それ自体がbundles
のリストであり、各バンドルのみがresources
のリストです(各バンドルの最大4つの要素)。 なぜこんな風にされたのですか? これには2つの目的があります。
- これにより、一意のbundleGroupの下にあるすべてのリソースを識別できます。 したがって、HTML応答にすべてのリソースを含める代わりに、対応するすべてのリソース内にバンドルする、対応するbundleGroupファイルである一意のJavaScriptアセットを含めることができます。 これは、まだHTTP / 2をサポートしていないデバイスにサービスを提供する場合に役立ちます。また、単一のバンドルファイルをGzipで圧縮する方が、構成ファイルを単独で圧縮してから合計するよりも効果的であるため、読み込み時間が長くなります。 または、一意のbundleGroupの代わりに一連のバンドルをロードすることもできます。これは、リソースとbundleGroupsの間の妥協点です(バンドルのロードはGzip化されているため、bundleGroupsよりも低速ですが、無効化が頻繁に発生する場合はパフォーマンスが向上するため、更新されたバンドルのみをダウンロードし、bundleGroup全体はダウンロードしません)。 すべてのリソースをバンドルおよびbundleGroupsにバンドルするためのスクリプトは、filegenerator-bundles.phpおよびfilegenerator-bundlegroups.phpにあります。
- リソースのセットをバンドルに分割すると、共通のパターンを識別でき(たとえば、多くのルート間で共有される4つのリソースのセットを識別)、その結果、異なるルートを同じバンドルにリンクできます。 その結果、生成されるリストのサイズは小さくなります。 これは、サーバー上にあるWebページリストにはあまり役立ちませんが、後で説明するように、クライアントに読み込まれるAPIリストには役立ちます。
APIモードのスクリプトを実行すると、次のプロパティを持つresources.jsファイルが出力されます。
-
bundles
とbundle-groups
は、Webページモードで説明したのと同じ目的を果たします keys
は、Webページモードのkey-ids
と同じ目的も果たします。 ただし、ルートを表すキーとしてハッシュを使用するのではなく、ルートを一意にするすべての属性(この場合は、フォーマット(f
)、タブ(t
)、およびターゲット(r
))を連結したものです。-
sources
は、各リソースのソースファイルです。 -
types
は各リソースのCSSまたはJavaScriptです(ただし、簡単にするために、JavaScriptリソースがCSSリソースを依存関係として設定し、モジュールが独自のCSSアセットをロードして、プログレッシブCSSロード戦略を実装することについてはこの記事では取り上げませんでした。 )。 -
resources
は、階層ごとにロードする必要のあるbundleGroupsをキャプチャします。 -
ordered-load-resources
には、依存するスクリプトの前にスクリプトがロードされないようにするために、どのリソースを順番にロードする必要があるかが含まれています(デフォルトでは非同期です)。
次のセクションでは、このファイルの使用方法について説明します。
4.アセットを動的にロードする
前述のように、APIリストはクライアントに読み込まれるため、ユーザーがリンクをクリックした直後に、ルートに必要なアセットの読み込みを開始できます。
マッピングスクリプトの読み込み
アプリケーション内のすべてのルートのリソースのリストを含む生成されたJavaScriptファイルは軽量ではありません。この場合、85 KBになりました(これ自体が最適化され、リソース名を変更し、ルート全体の共通パターンを識別するためのバンドルを生成しました) 。 JSONの解析は同じデータのJavaScriptの解析よりも10倍高速であるため、解析時間は大きなボトルネックにはなりません。 ただし、サイズはネットワーク転送の問題であるため、アプリケーションの認識される読み込み時間に影響を与えない方法でこのスクリプトを読み込むか、ユーザーを待たせる必要があります。
私が実装した解決策は、Service Workerを使用してこのファイルをプリキャッシュし、 defer
を使用してロードして、重要なJavaScriptメソッドの実行中にメインスレッドをブロックしないようにし、ユーザーがリンクをクリックした場合にフォールバック通知メッセージを表示することです。スクリプトが読み込まれる前に:「ウェブサイトはまだ読み込まれています。しばらく待ってからリンクをクリックしてください。」 これは、スクリプトのロード中にすべての上にloadingscreen
のクラスを配置した固定divを追加し、次にdiv内にnotificationmsg
のクラスを含む通知メッセージを追加し、CSSの次の数行を追加することで実現されます。
.loadingscreen > .notificationmsg { display: none; } .loadingscreen:focus > .notificationmsg, .loadingscreen:active > .notificationmsg { display: block; }
別の解決策は、このファイルをいくつかのファイルに分割し、必要に応じて段階的にロードすることです(私がすでにコーディングした戦略)。 さらに、85 KBのファイルには、「モーダルウィンドウに表示される、サムネイルで表示される作成者のアナウンス」など、アプリケーションで可能なすべてのルートが含まれています。 ほとんどアクセスされるルートはごくわずかであり(ホームページ、シングル、作成者、タグ、およびすべてのページ、追加の属性なしですべて)、30KB付近のはるかに小さいファイルを生成するはずです。
要求されたURLからルートを取得する
要求されたURLからルートを識別できる必要があります。 例えば:
-
https://getpop.org/en/u/leo/
は、ルート「作成者」にマップされます。 -
https://getpop.org/en/u/leo/?tab=followers
は、ルート「作成者のフォロワー」にマップします。 -
https://getpop.org/en/tags/internet/
は、ルート「タグ」にマップされます。 -
https://getpop.org/en/tags/
はルート「page/tags/
」にマップされ、 - 等々。
これを実現するには、URLを評価し、そこからルートを一意にする要素(階層とすべての属性(フォーマット、タブ、ターゲット))を推測する必要があります。 属性はURLのパラメータであるため、属性の識別は問題ありません。 唯一の課題は、URLをいくつかのパターンに一致させることにより、URLから階層(ホーム、作成者、単一、ページ、またはタグ)を推測することです。 例えば、
-
https://getpop.org/en/u/
で始まるものはすべて作成者です。 - https://getpop.org/en/tags/で始まるが、正確には
https://getpop.org/en/tags/
ではないものはすべてタグです。 正確にhttps://getpop.org/en/tags/
の場合は、ページです。 - 等々。
以下の関数は、resourceloader.jsの321行目から実装されており、これらすべての階層のパターンを使用した構成を提供する必要があります。 最初に、URLにサブパスがないかどうかをチェックします。この場合、それは「ホーム」です。 次に、「作成者」、「タグ」、「単一」の階層が一致するかどうかを1つずつチェックします。 これらのいずれでも成功しない場合は、デフォルトのケースである「ページ」です。
window.popResourceLoader = { // The config will be populated externally, using a config.js file, generated by a script config : {}, getPath : function(url) { var parser = document.createElement('a'); parser.href = url; return parser.pathname; }, getHierarchy : function(url) { var path = this.getPath(url); if (!path) { return 'home'; } var config = this.config; if (path.startsWith(config.paths.author) && path != config.paths.author) { return 'author'; } if (path.startsWith(config.paths.tag) && path != config.paths.tag) { return 'tag'; } // We must also check that this path is, itself, not a potential page (https://getpop.org/en/posts/articles/ is "page", but https://getpop.org/en/posts/this-is-a-post/ is "single") if (config.paths.single.indexOf(path) === -1 && config.paths.single.some(function(single_path) { return path.startsWith(single_path) && path != single_path;})) { return 'single'; } return 'page'; }, ... };
必要なすべてのデータ(すべてのカテゴリ、すべてのページスラッグなど)がすでにデータベースにあるため、スクリプトを実行して、開発環境またはステージング環境でこの構成ファイルを自動的に作成します。 The implemented script is resourceloader-config.php, which produces config.js with the URL patterns for the hierarchies “author”, “tag” and “single”, under the key “paths”:
popResourceLoader.config = { "paths": { "author": "u/", "tag": "tags/", "single": ["posts/articles/", "posts/announcements/", ...] }, ... };
Loading Resources for the Route
Once we have identified the route, we can obtain the required assets from the generated JavaScript file under the key “resources”, which looks like this:
config.resources = { "home": { "1": [1, 110, ...], "2": [2, 111, ...], ... }, "author": { "7": [6, 114, ...], "8": [7, 114, ...], ... }, "tag": { "119": [66, 127, ...], "120": [66, 127, ...], ... }, "single": { "posts/": { "7": [190, 142, ...], "3": [190, 142, ...], ... }, "events/": { "7": [213, 389, ...], "3": [213, 389, ...], ... }, ... }, "page": { "log-in/": { "3": [233, 115, ...] }, "log-out/": { "3": [234, 115, ...] }, "add-post/": { "3": [239, 398, ...] }, "posts/": { "120": [268, 127, ...], "122": [268, 127, ...], ... }, ... } };
At the first level, we have the hierarchy (home, author, tag, single or page). Hierarchies are divided into two groups: those that have only one set of resources (home, author and tag), and those that have a specific subpath (page permalink for the pages, custom post type or category for the single). Finally, at the last level, for each key ID (which represents a unique combination of the possible values of “format”, “tab” and “target”, stored under “keys”), we have an array of two elements: [JS bundleGroup ID, CSS bundleGroup ID], plus additional bundleGroup IDs if executing progressive booting (JS bundleGroups to be loaded as "async" or "defer" are bundled separately; this will be explained in the optimizations section below).
Please note: For the single
hierarchy, we have different configurations depending on the custom post type. This can be reflected in the subpath indicated above (for example, events
and posts
) because this information is in the URL (for example, https://getpop.org/en/posts/the-winners-of-climate-change-techno-fixes/
and https://getpop.org/en/events/debate-post-fork/
), so that, when clicking on a link, we will know the corresponding post type and can thus infer the corresponding route. However, this is not the case with the author
hierarchy. As indicated earlier, an author may have three different configurations, depending on the user role ( individual
, organization
or community
); however, in this file, we've defined only one configuration for the author hierarchy, not three. That is because we are not able to tell from the URL what is the role of the author: user leo
(under https://getpop.org/en/u/leo/
) is an individual, whereas user pop
(under https://getpop.org/en/u/pop/
) is a community; however, their URLs have the same pattern. If we could instead have the URLs https://getpop.org/en/u/individuals/leo/
and https://getpop.org/en/u/communities/pop/
, then we could add a configuration for each user role. However, I've found no way to achieve this in WordPress. As a consequence, only for the API mode, we must merge the three routes (individuals, organizations and communities) into one, which will have all of the resources for the three cases; and clicking on the link for user leo
will also load the resources for organizations and communities, even if we don't need them.
Finally, when a URL is requested, we obtain its route, from which we obtain the bundleGroup IDs (for both JavaScript and CSS assets). From each bundleGroup, we find the corresponding bundles under bundlegroups
. Then, for each bundle, we obtain all resources under the key bundles
. Finally, we identify which assets have not yet been loaded, and we load them by getting their source, which is stored under the key sources
. The whole logic is coded starting from line 472 in resourceloader.js.
And with that, we have implemented code-splitting for our application! From now on, we can get better loading times by applying optimizations. 次にそれに取り組みましょう。
5. Applying Optimizations
The objective is to load as little code as possible, as delayed as possible, and to cache as much of it as possible. これを行う方法を調べてみましょう。
Splitting Up the Code Into Smaller Units
A single JavaScript asset may implement several functions (by calling popJSLibraryManager.register
), yet maybe only one of those functions is actually needed by the route. Thus, it makes sense to split up the asset into several subassets, implementing a single function on each of them, and extracting all common code from all of the functions into yet another asset, depended upon by all of them.
For instance, in the past, there was a unique file, waypoints.js
, that implemented the functions waypointsFetchMore
, waypointsTheater
and a few more. However, in most cases, only the function waypointsFetchMore
was needed, so I was loading the code for the function waypointsTheater
unnecessarily. Then, I split up waypoints.js
into the following assets:
- waypoints.js, with all common code and implementing no public functions;
- waypoints-fetchmore.js, which implements just the public function
waypointsFetchMore
; - waypoints-theater.js, which implements just the public function
waypointsTheater
.
Evaluating how to split the files is a manual job. Luckily, there is a tool that greatly eases the task: Chrome Developer Tools' “Coverage” tab, which displays in red those portions of JavaScript code that have not been invoked:
By using this tool, we can better understand how to split our JavaScript files into more granular units, thus reducing the amount of unneeded code that is loaded.
Integration With Service Workers
By precaching all of the resources using service workers, we can be pretty sure that, by the time the response is back from the server, all of the required assets will have been loaded and parsed. I wrote an article on Smashing Magazine on how to accomplish this.
Progressive Booting
PoP's architecture plays very nice with the concept of loading assets in different stages. When defining the JavaScript methods to execute on each module (by doing $this->add_jsmethod($methods, 'calendar')
), these can be set as either critical
or non-critical
. By default, all methods are set as non-critical, and critical methods must be explicitly defined by the developer, by adding an extra parameter: $this->add_jsmethod($methods, 'calendar', 'critical')
. Then, we will be able to load scripts immediately for critical functions, and wait until the page is loaded to load non-critical functions, the JavaScript files of which are loaded using defer
.
(function($){ window.popManager = { init : function() { var that = this; $.each(this.configuration, function(pageSectionId, configuration) { ... this.runJSMethods(pageSection, configuration, 'critical'); ... }); window.addEventListener('load', function() { $.each(this.configuration, function(pageSectionId, configuration) { ... this.runJSMethods(pageSection, configuration, 'non-critical'); ... }); }); ... }, ... }; })(jQuery);
The gains from progressive booting are major: The JavaScript engine needs not spend time parsing non-critical JavaScript initially, when a quick response to the user is most important, and overall reduces the time to interactive.
Testing And Analizying Performance Gains
We can use https://getpop.org/en/, a PoP website, for testing purposes. When loading the home page, opening Chrome Developer Tools' “Elements” tab and searching for “defer”, it shows 4 occurrences. Thanks to progressive booting, that is 4 bundleGroup JavaScript files containing the contents of 57 Javascript files with non-critical methods that could wait until the website finished loading to be loaded:
If we now switch to the “Network” tab and click on a link, we can see which assets get loaded. For instance, click on the link “Application/UX Features” on the left side. Filtering by JavaScript, we see it loaded 38 files, including JavaScript libraries and Handlebars templates. Filtering by CSS, we see it loaded 9 files. These 47 files have all been loaded on demand:
Let's check whether the loading time got boosted. We can use WebPagetest to measure the application with and without code-splitting, and calculate the difference.
- Without code-splitting: testing URL, WebPagetest results
- With code-splitting, loading resources: testing URL, WebPagetest Results
- With code-splitting, loading a bundleGroup: testing URL, WebPagetest Results
We can see that when loading the app bundle with all resources or when doing code-splitting and loading resources, there is not so much gain. However, when doing code-splitting and loading a bundleGroup, the gains are significant: 1.7 seconds in loading time, 500 milliseconds to the first meaningful paint, and 1 second to interactive.
Conclusion: Is It Worth It?
You might be thinking, Is it worth it all this trouble? Let's analyze the advantages and disadvantages of implementing our own code-splitting features.
短所
- 私たちはそれを維持しなければなりません。
Webpackを使用したばかりの場合は、そのコミュニティに依存してソフトウェアを最新の状態に保つことができ、プラグインエコシステムの恩恵を受けることができます。 - スクリプトの実行には時間がかかります。
PoP Webサイトのアジェンダアーバナには304の異なるルートがあり、そこから422セットの固有のリソースが生成されます。 このWebサイトでは、2012年のMacBook Proを使用してアセット依存マップを生成するスクリプトを実行するのに約8分かかり、すべてのリソースを含むリストを生成してバンドルファイルとbundleGroupファイルを作成するスクリプトを実行するのに15分かかります。 それはコーヒーを飲むのに十分な時間です! - ステージング環境が必要です。
スクリプトを実行するために約25分待つ必要がある場合、本番環境でスクリプトを実行することはできません。 本番システムとまったく同じ構成のステージング環境が必要です。 - 管理のためだけに、追加のコードがWebサイトに追加されます。
85 KBのコードはそれ自体では機能しませんが、他のコードを管理するためのコードにすぎません。 - 複雑さが追加されます。
資産をより小さな単位に分割したい場合、これはどのような場合でも避けられません。 Webpackは、アプリケーションを複雑にします。
利点
- WordPressで動作します。
WebpackはそのままではWordPressで動作しません。動作させるには、かなりの回避策が必要です。 このソリューションは、WordPressの箱から出してすぐに機能します(PoPがインストールされている限り)。 - スケーラブルで拡張可能です。
JavaScriptファイルはオンデマンドで読み込まれるため、アプリケーションのサイズと複雑さは際限なく大きくなる可能性があります。 - グーテンベルク(別名、明日のワードプレス)をサポートしています。
JavaScriptフレームワークをオンデマンドでロードできるため、開発者が選択したフレームワークでコーディングされることが期待されるグーテンベルクのブロック(Gutenblocksと呼ばれる)をサポートし、同じアプリケーションに異なるフレームワークが必要になる可能性があります。 - 便利です。
ビルドツールは、構成ファイルの生成を処理します。 待つことを除けば、私たちからの余分な努力は必要ありません。 - 最適化が容易になります。
現在、WordPressプラグインがJavaScriptアセットを選択的にロードしたい場合、ページIDが正しいかどうかをチェックするために多くの条件を使用します。 このツールでは、その必要はありません。 プロセスは自動です。 - アプリケーションはより速くロードされます。
これが、このツールをコーディングした理由です。 - ステージング環境が必要です。
プラスの副作用は信頼性の向上です。本番環境ではスクリプトを実行しないため、そこで何も壊すことはありません。 展開プロセスが予期しない動作で失敗することはありません。 開発者は、本番環境と同じ構成を使用してアプリケーションをテストする必要があります。 - アプリケーションに合わせてカスタマイズされています。
オーバーヘッドや回避策はありません。 私たちが得ているのは、私たちが取り組んでいるアーキテクチャに基づいて、まさに私たちが必要としているものです。
結論:はい、それだけの価値があります。WordPressWebサイトでロードアセットをオンデマンドで適用し、ロードを高速化できるようになったからです。
その他のリソース
- 「コード分割」ガイドを含むWebpack
- 「BetterWebpackBuilds」(ビデオ)、K。AdamWhite
WebpackとWordPressの統合 - 「グーテンベルクと明日のワードプレス」、モルテンランドヘンドリクセン、WPタバーン
- 「WordPressはJavaScriptフレームワークにとらわれずにグーテンベルクブロックを構築するアプローチを模索しています」、WP Tavern、Sarah Gooding