自動的にインライン化されたコードの落とし穴を回避する

公開: 2022-03-10
簡単な要約↬静的リソースを介してコードを提供するのではなく、インラインCSSまたはJSコードを使いすぎると、サイトのパフォーマンスが低下する可能性があります。 この記事では、代わりに静的ファイルを介して動的コードをロードする方法を学習し、インラインコードが多すぎるという欠点を回避します。

インライン化は、ファイルのコンテンツをHTMLドキュメントに直接含めるプロセスです。CSSファイルはstyle要素内にインライン化でき、JavaScriptファイルはscript要素内にインライン化できます。

 <style> /* CSS contents here */ </style> <script> /* JS contents here */ </script>

すでにHTML出力にあるコードを印刷することにより、インライン化はレンダリングブロック要求を回避し、ページがレンダリングされる前にコードを実行します。 そのため、サイトの知覚パフォーマンス(つまり、ページが使用可能になるまでにかかる時間)を改善するのに役立ちます。たとえば、サイトをインラインにロードするときにすぐに配信されるデータのバッファー(約14kb)を使用できます。折り畳み上のコンテンツのスタイル(以前のSmashing Magazineサイトで行われていた)を含む重要なスタイル、および残りのデータが配信されるときにジャンプするレイアウトの再レンダリングを回避するためのフォントサイズとレイアウトの幅と高さ。

ただし、やりすぎると、コードのインライン化もサイトのパフォーマンスに悪影響を与える可能性があります。コードはキャッシュできないため、同じコンテンツがクライアントに繰り返し送信され、ServiceWorkerを介して事前にキャッシュすることはできません。キャッシュされ、コンテンツ配信ネットワークからアクセスされます。 さらに、コンテンツセキュリティポリシー(CSP)を実装する場合、インラインスクリプトは安全ではないと見なされます。 次に、CSSとJSの重要な部分をインライン化して、サイトの読み込みを高速化しますが、それ以外の場合は可能な限り回避するという賢明な戦略を立てます。

インライン化を回避する目的で、この記事では、インラインコードを静的アセットに変換する方法を探ります。HTML出力でコードを出力する代わりに、コードをディスクに保存し(事実上静的ファイルを作成し)、対応する<script>を追加します。 <script>または<link>タグを使用して、ファイルをロードします。

始めましょう!

推奨読書プロセスとしてのWordPressセキュリティ

ジャンプした後もっと! 以下を読み続けてください↓

インライン化を避ける場合

一部のコードをインライン化する必要があるかどうかを確認するための魔法のレシピはありませんが、一部のコードをインライン化してはならない場合、つまり、コードの大きなチャンクが含まれる場合、およびすぐに必要とされない場合は、かなり明白です。

例として、WordPressサイトはJavaScriptテンプレートをインライン化してMedia Manager( /wp-admin/upload.phpの下のMedia Libraryページからアクセス可能)をレンダリングし、かなりの量のコードを印刷します。

メディアライブラリページのソースコードのスクリーンショット
WordPress MediaManagerによってインライン化されたJavaScriptテンプレート。

43kb全体を占めるため、このコードのサイズは無視できません。また、ページの下部にあるため、すぐに必要になることはありません。 したがって、代わりに静的アセットを介してこのコードを提供するか、HTML出力内に印刷することは十分に理にかなっています。

次に、インラインコードを静的アセットに変換する方法を見てみましょう。

静的ファイルの作成のトリガー

コンテンツ(インライン化されるコンテンツ)が静的ファイルからのものである場合、コードをインライン化する代わりに、その静的ファイルを要求する以外に行うことはほとんどありません。

ただし、動的コードの場合は、静的ファイルとその内容をいつどのように生成するかを計画する必要があります。 たとえば、サイトが構成オプション(配色や背景画像の変更など)を提供している場合、新しい値を含むファイルをいつ生成する必要がありますか? 動的コードから静的ファイルを作成するには、次の機会があります。

  1. 要求に応じて
    ユーザーが初めてコンテンツにアクセスしたとき。
  2. 変更時
    動的コードのソース(構成値など)が変更されたとき。

まずはリクエストに応じて考えてみましょう。 ユーザーが初めてサイトにアクセスするとき、たとえば/index.htmlを介して、静的ファイル( header-colors.css )はまだ存在しないため、生成する必要があります。 イベントのシーケンスは次のとおりです。

  1. ユーザーが/index.htmlをリクエストします;
  2. リクエストを処理するとき、サーバーはファイルheader-colors.cssが存在するかどうかを確認します。 そうでないため、ソースコードを取得し、ディスク上にファイルを生成します。
  3. タグ<link rel="stylesheet" type="text/css" href="/staticfiles/header-colors.css">を含む応答をクライアントに返します。
  4. ブラウザは、 header-colors.cssを含むページに含まれるすべてのリソースをフェッチします。
  5. それまでにこのファイルが存在するため、提供されます。

ただし、イベントの順序も異なる可能性があり、不十分な結果につながる可能性があります。 例えば:

  1. ユーザーが/index.htmlをリクエストします;
  2. このファイルはブラウザー(または他のプロキシー、またはService Workers)によって既にキャッシュされているため、要求がサーバーに送信されることはありません。
  3. ブラウザは、 header-colors.cssを含む、ページに含まれるすべてのリソースをフェッチします。 ただし、この画像はブラウザにキャッシュされないため、リクエストはサーバーに送信されます。
  4. サーバーはまだheader-colors.cssを生成していません(たとえば、再起動したばかりです)。
  5. 404を返します。

または、/ /index.htmlをリクエストするときではなく、/ header- /header-colors.css自体をリクエストするときに、 header-colors.cssを生成することもできます。 ただし、このファイルは最初は存在しないため、リクエストはすでに404として扱われます。このファイルをハックして、ヘッダーを変更してステータスコードを200に変更し、画像のコンテンツを返すことはできますが、これは物事を行うためのひどい方法なので、私たちはこの可能性を楽しまないでしょう(私たちはこれよりもはるかに優れています!)

それはただ1つのオプションを残します:そのソースが変更された後に静的ファイルを生成することです。

ソースが変更されたときに静的ファイルを作成する

ユーザー依存とサイト依存の両方のソースから動的コードを作成できることに注意してください。 たとえば、テーマでサイトの背景画像を変更でき、そのオプションがサイトの管理者によって構成されている場合、展開プロセスの一部として静的ファイルを生成できます。 一方、サイトでユーザーがプロファイルの背景画像を変更できる場合は、実行時に静的ファイルを生成する必要があります。

一言で言えば、次の2つのケースがあります。

  1. ユーザー構成
    このプロセスは、ユーザーが構成を更新したときにトリガーする必要があります。
  2. サイト構成
    管理者がサイトの構成を更新するとき、またはサイトを展開する前に、プロセスをトリガーする必要があります。

2つのケースを個別に検討すると、#2の場合、必要なテクノロジースタックでプロセスを設計できます。 ただし、2つの異なるソリューションを実装するのではなく、両方のケースに対処できる独自のソリューションを実装する必要があります。 また、#1から、静的ファイルを生成するプロセスを実行中のサイトでトリガーする必要があるため、サイトが実行されているのと同じテクノロジースタックを中心にこのプロセスを設計する必要があります。

プロセスを設計するとき、コードは#1と#2の両方の特定の状況を処理する必要があります。

  • バージョニング
    新しい静的ファイルの作成時に前のファイルを無効にするには、静的ファイルに「バージョン」パラメーターを使用してアクセスする必要があります。 #2は単にサイトと同じバージョン管理にすることができますが、#1はユーザーごとに動的バージョンを使用する必要があり、データベースに保存されている可能性があります。
  • 生成されたファイルの場所
    #2はサイト全体の一意の静的ファイル(例: /staticfiles/header-colors.css )を生成し、#1は各ユーザーの静的ファイル(例: /staticfiles/users/leo/header-colors.css )を生成します。
  • トリガーイベント
    #1の場合、静的ファイルは実行時に実行する必要がありますが、#2の場合、ステージング環境のビルドプロセスの一部として実行することもできます。
  • 展開と配布
    #2の静的ファイルは、サイトの展開バンドル内にシームレスに統合できるため、問題は発生しません。 ただし、#1の静的ファイルは処理できないため、プロセスは、ロードバランサーの背後にある複数のサーバーなど、追加の懸念事項を処理する必要があります(静的ファイルは、1つのサーバーのみで作成されるのか、それともすべてのサーバーで作成されるのか、またどのように作成されるのでしょうか)。

次に、プロセスを設計して実装しましょう。 生成される静的ファイルごとに、ファイルのメタデータを含むオブジェクトを作成し、動的ソースからそのコンテンツを計算して、最後に静的ファイルをディスクに保存する必要があります。 以下の説明をガイドするユースケースとして、次の静的ファイルを生成します。

  1. header-colors.css 、データベースに保存された値からのいくつかのスタイル
  2. welcomeuser-data.js 、変数の下にユーザーデータを含むJSONオブジェクトが含まれています: window.welcomeUserData = {name: "Leo"};

以下では、WordPressの静的ファイルを生成するプロセスについて説明します。このため、PHPおよびWordPress関数に基づいてスタックを作成する必要があります。 以前の記事で説明したように、展開前に静的ファイルを生成する関数は、ショートコード[create_static_files]を実行する特別なページをロードすることでトリガーできます。

さらに推奨される読み物サービスワーカーの作成:ケーススタディ

ファイルをオブジェクトとして表現する

対応するすべてのプロパティを持つPHPオブジェクトとしてファイルをモデル化する必要があります。これにより、ファイルをディスク上の特定の場所(たとえば、 /staticfiles/または/staticfiles/users/leo/の下)に保存し、リクエストする方法を知ることができます。結果としてファイルします。 このために、ファイルのメタデータ(ファイル名、ディレクトリ、タイプ:「css」または「js」、バージョン、および他のリソースへの依存関係)とそのコンテンツの両方を返すインターフェイスResourceを作成します。

 interface Resource { function get_filename(); function get_dir(); function get_type(); function get_version(); function get_dependencies(); function get_content(); }

コードを保守可能で再利用可能にするために、SOLIDの原則に従います。この原則では、すべてのResource実装が継承する抽象クラスResourceBaseから始めて、リソースのオブジェクト継承スキームを設定し、プロパティを徐々に追加します。

 abstract class ResourceBase implements Resource { function get_dependencies() { // By default, a file has no dependencies return array(); } }

SOLIDに続いて、プロパティが異なる場合は常にサブクラスを作成します。 前述のように、生成された静的ファイルの場所と、それを要求するバージョンは、ユーザーまたはサイトの構成に関するファイルによって異なります。

 abstract class UserResourceBase extends ResourceBase { function get_dir() { // A different file and folder for each user $user = wp_get_current_user(); return "/staticfiles/users/{$user->user_login}/"; } function get_version() { // Save the resource version for the user under her meta data. // When the file is regenerated, must execute `update_user_meta` to increase the version number $user_id = get_current_user_id(); $meta_key = "resource_version_".$this->get_filename(); return get_user_meta($user_id, $meta_key, true); } } abstract class SiteResourceBase extends ResourceBase { function get_dir() { // All files are placed in the same folder return "/staticfiles/"; } function get_version() { // Same versioning as the site, assumed defined under a constant return SITE_VERSION; } }

最後に、最後のレベルで、生成するファイルのオブジェクトを実装し、ファイル名、ファイルのタイプ、および動的コードを関数get_contentを介して追加します。

 class HeaderColorsSiteResource extends SiteResourceBase { function get_filename() { return "header-colors"; } function get_type() { return "css"; } function get_content() { return sprintf( " .site-title a { color: #%s; } ", esc_attr(get_header_textcolor()) ); } } class WelcomeUserDataUserResource extends UserResourceBase { function get_filename() { return "welcomeuser-data"; } function get_type() { return "js"; } function get_content() { $user = wp_get_current_user(); return sprintf( "window.welcomeUserData = %s;", json_encode( array( "name" => $user->display_name ) ) ); } }

これにより、ファイルをPHPオブジェクトとしてモデル化しました。 次に、それをディスクに保存する必要があります。

静的ファイルをディスクに保存する

ファイルをディスクに保存することは、言語によって提供されるネイティブ関数を介して簡単に実行できます。 PHPの場合、これは関数fwriteを介して実行されます。 さらに、ディスク上のファイルへの絶対パスと、サイトのルートからの相対パスを提供する関数を使用して、ユーティリティクラスResourceUtilsを作成します。

 class ResourceUtils { protected static function get_file_relative_path($fileObject) { return $fileObject->get_dir().$fileObject->get_filename().".".$fileObject->get_type(); } static function get_file_path($fileObject) { // Notice that we must add constant WP_CONTENT_DIR to make the path absolute when saving the file return WP_CONTENT_DIR.self::get_file_relative_path($fileObject); } } class ResourceGenerator { static function save($fileObject) { $file_path = ResourceUtils::get_file_path($fileObject); $handle = fopen($file_path, "wb"); $numbytes = fwrite($handle, $fileObject->get_content()); fclose($handle); } }

次に、ソースが変更され、静的ファイルを再生成する必要がある場合は常に、 ResourceGenerator::saveを実行して、ファイルを表すオブジェクトをパラメーターとして渡します。 以下のコードは、ファイル「header-colors.css」と「welcomeuser-data.js」を再生成してディスクに保存します。

 // When need to regenerate header-colors.css, execute: ResourceGenerator::save(new HeaderColorsSiteResource()); // When need to regenerate welcomeuser-data.js, execute: ResourceGenerator::save(new WelcomeUserDataUserResource());

それらが存在すると、 <script> >タグと<link>タグを介してロードされるファイルをキューに入れることができます。

静的ファイルのエンキュー

静的ファイルのエンキューは、WordPressのリソースのエンキューと同じです。関数wp_enqueue_scriptおよびwp_enqueue_styleを使用します。 次に、すべてのオブジェクトインスタンスを繰り返し処理し、 get_type()の値が"js"または"css"であるかどうかに応じて、いずれかのフックを使用します。

まず、ユーティリティ関数を追加して、ファイルのURLを提供し、タイプがJSまたはCSSのいずれかであることを示します。

 class ResourceUtils { // Continued from above... static function get_file_url($fileObject) { // Add the site URL before the file path return get_site_url().self::get_file_relative_path($fileObject); } static function is_css($fileObject) { return $fileObject->get_type() == "css"; } static function is_js($fileObject) { return $fileObject->get_type() == "js"; } }

ResourceEnqueuerクラスのインスタンスには、ロードする必要のあるすべてのファイルが含まれます。 呼び出されると、その関数enqueue_scriptsenqueue_stylesは、対応するWordPress関数(それぞれwp_enqueue_scriptwp_enqueue_style )を実行することにより、エンキューを実行します。

 class ResourceEnqueuer { protected $fileObjects; function __construct($fileObjects) { $this->fileObjects = $fileObjects; } protected function get_file_properties($fileObject) { $handle = $fileObject->get_filename(); $url = ResourceUtils::get_file_url($fileObject); $dependencies = $fileObject->get_dependencies(); $version = $fileObject->get_version(); return array($handle, $url, $dependencies, $version); } function enqueue_scripts() { $jsFileObjects = array_map(array(ResourceUtils::class, 'is_js'), $this->fileObjects); foreach ($jsFileObjects as $fileObject) { list($handle, $url, $dependencies, $version) = $this->get_file_properties($fileObject); wp_register_script($handle, $url, $dependencies, $version); wp_enqueue_script($handle); } } function enqueue_styles() { $cssFileObjects = array_map(array(ResourceUtils::class, 'is_css'), $this->fileObjects); foreach ($cssFileObjects as $fileObject) { list($handle, $url, $dependencies, $version) = $this->get_file_properties($fileObject); wp_register_style($handle, $url, $dependencies, $version); wp_enqueue_style($handle); } } }

最後に、各ファイルを表すPHPオブジェクトのリストを使用してクラスResourceEnqueuerのオブジェクトをインスタンス化し、WordPressフックを追加してエンキューを実行します。

 // Initialize with the corresponding object instances for each file to enqueue $fileEnqueuer = new ResourceEnqueuer( array( new HeaderColorsSiteResource(), new WelcomeUserDataUserResource() ) ); // Add the WordPress hooks to enqueue the resources add_action('wp_enqueue_scripts', array($fileEnqueuer, 'enqueue_scripts')); add_action('wp_print_styles', array($fileEnqueuer, 'enqueue_styles'));

それだけです。キューに入れられると、クライアントにサイトをロードするときに静的ファイルが要求されます。 代わりに、インラインコードの印刷と静的リソースの読み込みを回避することに成功しました。

次に、パフォーマンスをさらに向上させるためにいくつかの改善を適用できます。

推奨読書PHPUnitを使用したWordPressプラグインの自動テストの概要

ファイルをまとめる

HTTP / 2によってファイルのバンドルの必要性が減りましたが、ファイルの圧縮(GZipなどによる)がより効果的になり、ブラウザー(Chromeなど)のオーバーヘッドが大きくなり、多くのリソースを処理できるため、サイトが高速になります。 。

これまでに、ファイルをPHPオブジェクトとしてモデル化しました。これにより、このオブジェクトを他のプロセスへの入力として扱うことができます。 特に、上記と同じプロセスを繰り返して、同じタイプのすべてのファイルをバンドルし、すべての独立したファイルの代わりにバンドルされたバージョンを提供できます。 このために、 $fileObjectsの下のすべてのリソースからコンテンツを抽出し、それを再度出力して、すべてのリソースからのすべてのコンテンツの集約を生成する関数get_contentを作成します。

 abstract class SiteBundleBase extends SiteResourceBase { protected $fileObjects; function __construct($fileObjects) { $this->fileObjects = $fileObjects; } function get_content() { $content = ""; foreach ($this->fileObjects as $fileObject) { $content .= $fileObject->get_content().PHP_EOL; } return $content; } }

このファイルのクラスを作成することで、すべてのファイルをバンドルされたファイルbundled-styles.cssにまとめることができます。

 class StylesSiteBundle extends SiteBundleBase { function get_filename() { return "bundled-styles"; } function get_type() { return "css"; } }

最後に、すべての独立したリソースではなく、以前のように、これらのバンドルされたファイルを単純にキューに入れます。 CSSの場合、ファイルheader-colors.cssbackground-image.cssfont-sizes.cssを含むバンドルを作成します。これらのファイルごとに、PHPオブジェクトを使用してStylesSiteBundleをインスタンス化するだけです(同様に、JSを作成できます)。バンドルファイル):

 $fileObjects = array( // CSS new HeaderColorsSiteResource(), new BackgroundImageSiteResource(), new FontSizesSiteResource(), // JS new WelcomeUserDataUserResource(), new UserShoppingItemsUserResource() ); $cssFileObjects = array_map(array(ResourceUtils::class, 'is_css'), $fileObjects); $jsFileObjects = array_map(array(ResourceUtils::class, 'is_js'), $fileObjects); // Use this definition of $fileEnqueuer instead of the previous one $fileEnqueuer = new ResourceEnqueuer( array( new StylesSiteBundle($cssFileObjects), new ScriptsSiteBundle($jsFileObjects) ) );

それでおしまい。 これで、多くではなく、1つのJSファイルと1つのCSSファイルのみを要求します。

知覚されるパフォーマンスの最終的な改善には、すぐに必要とされないアセットのロードを遅らせることにより、アセットに優先順位を付けることが含まれます。 次にこれに取り組みましょう。

JSリソースのasync / defer属性

属性をasyncに追加して<script>タグにdeferし、JavaScriptファイルのダウンロード、解析、実行のタイミングを変更して、重要なJavaScriptに優先順位を付け、重要でないものすべてをできるだけ遅くプッシュして、サイトの見かけの読み込みを減らすことができます。時間。

この機能を実装するには、SOLIDの原則に従って、関数is_asyncおよびis_deferを含む新しいインターフェイスJSResourceResourceから継承)を作成する必要があります。 ただし、これにより、最終的にこれらの属性もサポートする<style>タグへの扉が閉じられます。 したがって、適応性を念頭に置いて、よりオープンエンドなアプローチを採用します。ジェネリックメソッドget_attributesをインターフェイスResourceに追加するだけで、両方の属性(既存の属性またはまだ発明されていない属性)に柔軟に追加でき<script><script>および<link>タグ:

 interface Resource { // Continued from above... function get_attributes(); } abstract class ResourceBase implements Resource { // Continued from above... function get_attributes() { // By default, no extra attributes return ''; } }

WordPressは、キューに入れられたリソースに属性を追加する簡単な方法を提供していません。そのため、関数add_script_tag_attributesを介してタグ内の文字列を置き換えるフックを追加するというかなりハックな方法で追加します。

 class ResourceEnqueuerUtils { protected static tag_attributes = array(); static function add_tag_attributes($handle, $attributes) { self::tag_attributes[$handle] = $attributes; } static function add_script_tag_attributes($tag, $handle, $src) { if ($attributes = self::tag_attributes[$handle]) { $tag = str_replace( " src='${src}'>", " src='${src}' ".$attributes.">", $tag ); } return $tag; } } // Initize by connecting to the WordPress hook add_filter( 'script_loader_tag', array(ResourceEnqueuerUtils::class, 'add_script_tag_attributes'), PHP_INT_MAX, 3 );

対応するオブジェクトインスタンスを作成するときに、リソースの属性を追加します。

 abstract class ResourceBase implements Resource { // Continued from above... function __construct() { ResourceEnqueuerUtils::add_tag_attributes($this->get_filename(), $this->get_attributes()); } }

最後に、リソースwelcomeuser-data.jsをすぐに実行する必要がない場合は、 deferとして設定できます。

 class WelcomeUserDataUserResource extends UserResourceBase { // Continued from above... function get_attributes() { return "defer='defer'"; } }

スクリプトは遅延としてロードされるため、スクリプトは後でロードされ、ユーザーがサイトを操作できる時点が繰り越されます。 パフォーマンスの向上に関しては、これで準備が整いました。

リラックスする前に解決しなければならない問題が1つあります。それは、サイトが複数のサーバーでホストされている場合にどうなるかということです。

ロードバランサーの背後にある複数のサーバーの処理

私たちのサイトがロードバランサーの背後にある複数のサイトでホストされており、ユーザー構成に依存するファイルが再生成される場合、リクエストを処理するサーバーは、何らかの方法で、再生成された静的ファイルを他のすべてのサーバーにアップロードする必要があります。 それ以外の場合、他のサーバーはその時点からそのファイルの古いバージョンを提供します。 これをどのように行うのですか? サーバーを相互に通信させることは複雑であるだけでなく、最終的には実行不可能になる可能性があります。サイトがさまざまな地域の数百台のサーバーで実行されている場合はどうなりますか? 明らかに、これはオプションではありません。

私が思いついた解決策は、間接参照のレベルを追加することです。サイトのURLから静的ファイルをリクエストする代わりに、AWSS3バケットなどのクラウド内の場所からリクエストされます。 次に、ファイルを再生成すると、サーバーはすぐに新しいファイルをS3にアップロードし、そこから提供します。 このソリューションの実装については、前回の記事「AWSS3を介した複数のサーバー間でのデータの共有」で説明されています。

結論

この記事では、JSコードとCSSコードのインライン化は必ずしも理想的ではないと考えました。これは、コードをクライアントに繰り返し送信する必要があるため、コードの量が多いとパフォーマンスに影響を与える可能性があるためです。 例として、WordPressが43kbのスクリプトをロードしてMedia Managerを印刷する方法を見ました。これは純粋なJavaScriptテンプレートであり、静的リソースとして完全にロードできます。

したがって、動的JSおよびCSSインラインコードを静的リソースに変換することでWebサイトを高速化する方法を考案しました。これにより、複数のレベル(クライアント、Service Worker、CDN)でのキャッシュを強化し、すべてのファイルをさらにバンドルできます。 1つのJS / CSSリソースに統合して、出力を圧縮するときの比率を改善し(GZipなどを介して)、ブラウザーで複数のリソースを同時に処理することによるオーバーヘッドを回避し(Chromeなど)、さらに属性をasyncまたはdeferできるようにします<script>タグに追加して、ユーザーの対話を高速化し、サイトの見かけの読み込み時間を改善します。

有益な副作用として、コードを静的リソースに分割すると、コードが読みやすくなり、HTMLの大きなブロブではなくコードの単位を処理できるようになり、プロジェクトのメンテナンスが向上します。

私たちが開発したソリューションはPHPで行われ、WordPressの特定のコードがいくつか含まれていますが、コード自体は非常に単純で、SOLIDの原則に従ってプロパティとそれらのプロパティを実装するオブジェクトを定義するインターフェイスはほとんどなく、ファイルをディスクに。 それはほとんどそれです。 最終結果は、クリーンでコンパクトで、他の言語やプラットフォーム用に簡単に再作成でき、既存のプロジェクトに導入するのは難しくありません。パフォーマンスを簡単に向上させることができます。