パフォーマンス

パフォーマンスは必要不可欠な要素であり、最新のアプリケーションの主な懸念事項の1つです。

モニタリング

Grafanaインスタンスの1つにパフォーマンスダッシュボードがあります。このダッシュボードは、4時間ごとにsitespeed.ioからメトリクスデータを自動的に集計します。これらの変更は、設定された数のページが集約された後に表示されます。

これらのページはgitlab というsitespeed-measurement-setup リポジトリ 内のテキストファイル内にあります。 どのフロントエンドエンジニアもこのダッシュボードに貢献することができます。ページの URL をテキストファイルに追加したり削除したりすることで貢献することができます。変更がmainにマージされた後、次のスケジュール実行時に変更がプッシュされます。

各ページでレビューするために、3つの推奨される影響度の高いメトリクス(コアウェブバイタル)があります:

これらのメトリクスでは、ウェブサイトのパフォーマンスが高いことを意味するため、数値が低いほど優れています。

ユーザータイミングAPI

ユーザータイミングAPIは、すべてのモダンブラウザで利用可能なウェブAPIです。コードに特別なマークを配置することで、アプリケーションのカスタム時間や持続時間を測定することができます。GitLabのUser Timing APIを使用すると、Rails、Vue、またはバニラJavaScript環境など、フレームワークに関係なく任意のタイミングを測定できます。採用の一貫性と利便性のために、GitLabはあなたのコードでカスタムユーザータイミングメトリクスを有効にするいくつかの方法を提供します。

User Timing APIは2つの重要なパラダイムを導入しています:markmeasure

マークはパフォーマンスタイムライン上のタイムスタンプです。例えば、performance.mark('my-component-start'); は、このコードが満たされた時間をブラウザに記録させます。その後、グローバル・パフォーマンス・オブジェクトを再度クエリすることで、このマークに関する情報を得ることができます。例えば、DevToolsコンソールで:

performance.getEntriesByName('my-component-start')

Measureは、次のどちらかの間の時間です:

  • つのマーク
  • ナビゲーションの開始とマーク
  • 航行開始と計測の瞬間

いくつかの引数をとりますが、必要なのは測定名だけです。例

  • 開始マークと終了マーク間の時間:

     performance.measure('My component', 'my-component-start', 'my-component-end')
    
  • マークから測定が行われるまでの時間。この場合、終了マークは省略されます。

     performance.measure('My component', 'my-component-start')
    
  • ナビゲーションの開始から実際の測定が行われるまでの時間。

     performance.measure('My component')
    
  • ナビゲーション開始からマークまでの時間。この場合、スタートマークを省略することはできませんが、undefined に設定することができます。

     performance.measure('My component', undefined, 'my-component-end')
    

特定のmeasure をクエリするには、mark と同じ API を使用できます:

performance.getEntriesByName('My component')

また、捕捉されたすべてのマークと測定値をクエリすることもできます:

performance.getEntriesByType('mark');
performance.getEntriesByType('measure');

getEntriesByName() またはgetEntriesByType() を使用すると、測定の開始時刻と継続時間に関する情報を含むPerformanceMeasure オブジェクトの配列が返されます。

ユーザー・タイミング API ユーティリティ

performanceMarkAndMeasure ユーティリティは特定の環境に縛られないので、GitLab のどこでも使うことができます。

performanceMarkAndMeasure オブジェクトを引数にとります:

属性種類必須説明
markStringいいえ設定するマークの名前。後でマークを検索する際に使用します。指定しない場合、マークは設定されません。
measuresArrayいいえこの時点で測定するリスト。

その代わり、measures 配列のエントリは、以下の API を持つオブジェクトとなります:

属性種類必須説明
nameStringyes測定の名前。後でマークを取得するために使用します。メジャー・オブジェクトごとに指定する必要があります。
startStringいいえ測定を行うマークの名前。
endStringいいえ測定を行うマークの名前。

使用例:

import { performanceMarkAndMeasure } from '~/performance/utils';
...
performanceMarkAndMeasure({
  mark: MR_DIFFS_MARK_DIFF_FILES_END,
  measures: [
    {
      name: MR_DIFFS_MEASURE_DIFF_FILES_DONE,
      start: MR_DIFFS_MARK_DIFF_FILES_START,
      end: MR_DIFFS_MARK_DIFF_FILES_END,
    },
  ],
});

Vueパフォーマンスプラグイン

このプラグインは、VueのライフサイクルとユーザータイミングAPIを活用して、指定されたVueコンポーネントのパフォーマンスを自動的に取得して測定します。

Vueパフォーマンスプラグインを使用するには

  1. プラグインをインポートします:

    import PerformancePlugin from '~/performance/vue_performance_plugin';
    
  2. Vueアプリケーションを初期化する前に使用します:

    Vue.use(PerformancePlugin, {
      components: [
        'IdeTreeList',
        'FileTree',
        'RepoEditor',
      ]
    });
    

このプラグインは、パフォーマンスを測定するコンポーネントのリストを受け付けます。コンポーネントはname オプションで指定します。

コードベースのほとんどのコンポーネントにはこのオプションが設定されていないので、必要なコンポーネントにこのオプションを明示的に設定する必要があるかもしれません:

export default {
  name: 'IdeTreeList',
  components: {
    ...
  ...
}

このプラグインは以下をキャプチャして保存します:

  • コンポーネントが初期化されたときの開始マーク(beforeCreate() フック)
  • コンポーネントがレンダリングされたときの終了マーク(mounted() フックではnextTick の次のアニメーションフレーム)。ほとんどの場合、このイベントは、すべてのサブコンポーネントが起動するまで待ちません。サブコンポーネントを測定するには、プラグインオプションにそれらを含める必要があります。
  • 上記の2つのマークの間の継続時間を測定してください。

保存された測定値へのアクセス

保存された測定値にアクセスするには、以下のいずれかを使用します:

  • パフォーマンス・バー。これを有効にしている場合 (P +B キーコンボ)、DevTools コンソールでメトリクスの出力を見ることができます。
  • DevToolsの“Performance “タブ。パフォーマンスをプロファイリングするとき、このタブで測定値を得ることができます(マークではありませんが)。
  • DevToolsコンソール。前述のように、エントリーをクエリできます:

     performance.getEntriesByType('mark');
     performance.getEntriesByType('measure');
    

命名規約

すべてのマークおよびメジャーは、app/assets/javascripts/performance/constants.js からの定数でインスタンス化する必要があります。新しいマークまたはメジャーのラベルを追加する準備ができたら、このパターンに従います。

note
このパターンは推奨であり、厳密なルールではありません。
app-*-start // for a start 'mark'
app-*-end   // for an end 'mark'
app-*       // for 'measure'

例えば、'webide-init-editor-startmr-diffs-mark-file-tree-end など。これは、同じページ上の異なるアプリから来たマークやメジャーを識別しやすくするためです。

ベストプラクティス

リアルタイム・コンポーネント

リアルタイム機能のコードを書くときには、いくつかのことを念頭に置かなければなりません:

  1. リクエストでサーバーに負荷をかけないこと。
  2. リアルタイムであるべきです。

そのため、リクエストの送信とリアルタイム感のバランスを取る必要があります。リアルタイム・ソリューションを作成する際には、以下のルールを使いましょう。

  1. サーバーは、ヘッダでPoll-Interval を送信することで、どれくらいポーリングするかを教えてくれます。それをポーリング間隔として使います。これにより、システム管理者はポーリング速度を変更することができます。Poll-Interval: -1 はポーリングを無効にすることを意味し、これを実装する必要があります。
  2. HTTPステータスが2XXと異なるレスポンスも同様にポーリングを無効にする必要があります。
  3. ポーリングには共通のライブラリを使用してください。
  4. アクティブなタブに対してのみポーリングを行います。可視性を使用します。
  5. 定期的なポーリング間隔を使用します。間隔はサーバーによって制御されるため、バックオフポーリングやジッターは使用しないでください。
  6. バックエンドのコードはETagsを使用している可能性が高いです。304 Not Modified をチェックする必要はありません。ブラウザが変換してくれます。

画像の遅延読み込み

最初のレンダリングにかかる時間を改善するために、画像の遅延読み込みを使用しています。これは、data-src 属性に data-src実際の画像ソースを設定することで機能します。data-src HTML がレンダリングされ、JavaScript が読み込まれた後、 data-src画像が現在のビューポート内にある場合、src に自動的に移動します。

  • src 属性の名前をdata-src に変更し、クラスlazy を追加することで、HTML内の画像を遅延読み込み用に準備します。
  • Railsimage_tag ヘルパーを使用している場合、lazy: false が提供されない限り、デフォルトですべての画像が遅延ロードされます。

遅延画像を含むコンテンツを非同期で追加する場合は、遅延画像を検索し、必要であればロードする関数gl.lazyLoader.searchLazyImages() を呼び出してください。一般的には、遅延ロード関数のMutationObserver を使って自動的に処理されるべきです。

アニメーション

opacitytransform プロパティだけをアニメーション化します。その他のプロパティ(topleftmarginpaddingなど)はすべて「レイアウト」の再計算を引き起こします。これについては、High Performance Animationsの「Styles that Affect Layout」を参照してください。

レイアウトを変更する必要が_ある_場合(例えば、サイドバーがメインコンテンツを押しのけるなど)は、FLIPをお勧めします。FLIPでは、高価なプロパティを一度変更するだけで、実際のアニメーションはトランスフォームで処理できます。

アセットのプリフェッチ

APIからのデータのプリフェッチに加えて、Webpackの設定で定義された名前付きJavaScriptの「チャンク」のプリフェッチも可能です。チャンクのプリフェッチには2種類あります:

  • prefetch リンクタイプ は、将来のナビゲーションのためにチャンクをプリフェッチするために使用されます。
  • preload リンクタイプ は、現在のナビゲーションにとって重要でありながら、レンダリングプロセスの後半になるまで発見されないチャンクをプリフェッチするために使用されます。

prefetchpreload の両リンクは、ページに読み込みパフォーマンスの利点をもたらします。どちらも非同期でフェッチされますが、デフォルトで製品の他の JavaScript リソースに使用されるアセットのロードの延期とは逆に、prefetchpreload は、JavaScript モジュールで明示的にインポートされない限り、フェッチされたスクリプトを解析も実行もしません。これにより、残りのページリソースの実行をブロックすることなく、フェッチされたリソースをキャッシュすることができます。

HAMLビューのJavaScriptチャンクをプリフェッチするために、webpack_preload_asset_tag ヘルパーを組み合わせた:prefetch_asset_tags

- content_for :prefetch_asset_tags do
  - webpack_preload_asset_tag('monaco')

このスニペットは結果の HTML ページに新しい<link rel="preload"> 要素を追加します:

<link rel="preload" href="/assets/webpack/monaco.chunk.js" as="script" type="text/javascript">

デフォルトでは、webpack_preload_asset_tag がチャンクをpreload します。JavaScriptのチャンクをプリロードするために、astype 属性を気にする必要はありません。しかし、チャンクが重要でない場合、現在のナビゲーションでは、明示的にprefetch をリクエストする必要があります:

- content_for :prefetch_asset_tags do
  - webpack_preload_asset_tag('monaco', prefetch: true)

このスニペットは結果の HTML ページに新しい<link rel="prefetch"> 要素を追加します:

<link rel="prefetch" href="/assets/webpack/monaco.chunk.js">

アセット・フットプリントの削減

ユニバーサルコード

main.jscommons/index.js に内部で含まれているコードは、_すべての_Pages で読み込まれ、実行されます。本当に_どこでも_必要でない限り、これらのファイルには何も追加しないでください。これらのバンドルには、vueaxiosjQueryのようなユビキタスライブラリや、メインナビゲーションやサイドバーのコードが含まれています。可能であれば、これらのバンドルからモジュールを削除して、コードのフットプリントを減らすことを目指しましょう。

ページ固有のJavaScript

Webpackは、app/assets/javascripts/pages/* のファイル構造に基づいてエントリポイントバンドルを自動生成するように設定されています。pages ディレクトリ内のディレクトリは Rails コントローラとアクションに対応しています。これらの自動生成されたバンドルは、対応するページに自動的にインクルードされます。

たとえば、https://gitlab.com/gitlab-org/gitlab/-/issues にアクセスすると、index アクションでapp/controllers/projects/issues_controller.rb コントローラにアクセスすることになります。pages/projects/issues/index/index.js に対応するファイルが存在する場合、そのファイルはwebpackバンドルにコンパイルされ、ページにインクルードされます。

以前は、GitLabはHAMLファイルでcontent_for :page_specific_javascripts 、手動で生成したwebpackバンドルを使うことを推奨していました。しかし、この新しいシステムでは、webpack.config.js ファイルに手動でエントリポイントを追加する必要はありません。

note
どのコントローラとアクションがどのページに対応しているのか不明な場合は、GitLab のどのページからでもブラウザの開発者コンソールでdocument.body.dataset.page を調べてください。

重要な考慮事項

  • エントリーポイントは常に軽く:ページ固有の JavaScript エントリーポイントは、可能な限り軽量であるべきです。これらのファイルはユニットテストの対象外であり、主にエントリーポイントスクリプトの外のモジュールに存在するクラスやメソッドのインスタンス化と依存性注入のために使用されるべきです。インポートし、DOM を読み、インスタンス化するだけで、あとは何もしません。

  • DOMContentLoaded は使用しないでください: すべてのGitLab JavaScriptファイルはdefer 属性で追加されます。Mozillaのドキュメントによると、これは「ドキュメントが解析された後、スクリプトが実行されるDOMContentLoadedDOMContentLoadedことを意味しています。DOMContentLoadedドキュメントはすでに解析されているので DOMContentLoaded、アプリケーションをブートストラップする必要はありません。

  • 計算のためにCSSに依存するJavaScriptはwaitForCSSLoaded(): GitLabはページのパフォーマンスを改善するためにStartup.cssを使います。JavaScriptが計算のためにCSSに依存している場合、これはイシューを引き起こす可能性があります。これを解決するには、JavaScriptをwaitForCSSLoaded() ヘルパー関数でラップします。

     import initMyWidget from './my_widget';
     import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
       
     waitForCSSLoaded(initMyWidget);
    

    waitForCSSLoaded() メソッドは様々な方法でアクションを受け取ることができます:

    • コールバック

         waitForCSSLoaded(action)
      
    • then()

         waitForCSSLoaded().then(action);
      
    • await の後にaction が続きます:

         await waitForCSSLoaded;
         action();
      

    例えば、app/assets/javascripts/pages/projects/graphs/charts/index.js の使い方をご覧ください:

     waitForCSSLoaded(() => {
       const languagesContainer = document.getElementById('js-languages-chart');
       //...
     });
    
  • モジュール配置のサポート
    • クラスやモジュールが_特定のルートに固有で_ある場合、それが使われるエントリーポイントの近くに配置するようにしてください。たとえば、my_widget.jspages/widget/show/index.js でのみインポートされる場合、pages/widget/show/my_widget.js にモジュールを配置し、相対パスでインポートする必要があります (たとえば、import initMyWidget from './my_widget';)。
    • クラスやモジュールが_複数のルートで使わ_れる場合、それをインポートするエントリーポイントに最も近い共通の親ディレクトリの共有ディレクトリに配置してください。たとえば、my_widget.jspages/widget/show/index.jspages/widget/run/index.js の両方でインポートされる場合、pages/widget/shared/my_widget.js にモジュールを配置し、可能であれば相対パスでインポートします (たとえば、../shared/my_widget)。
  • エンタープライズ版の注意点GitLab Enterprise Editionでは、ページ固有のエントリーポイントはCommunity Editionの同名のものを上書きします。そのため、ee/app/assets/javascripts/pages/foo/bar/index.js が存在する場合は、app/assets/javascripts/pages/foo/bar/index.jsより優先されます。コードの重複を最小限に抑えたい場合は、一方のエントリーポイントをもう一方のエントリーポイントからインポートすることができます。これは、機能を柔軟に上書きできるようにするため、自動的には行われません。

コードの分割

ページロード時に即座に実行する必要のないコード (たとえば、モーダル、ドロップダウン、その他の遅延ロード可能なビヘイビア) は、動的なインポート文を使用して非同期のチャンクに分割する必要があります。これらのインポートはスクリプトがロードされた後に解決されるPromiseを返します:

import(/* webpackChunkName: 'emoji' */ '~/emoji')
  .then(/* do something */)
  .catch(/* report error */)

動的インポートを生成するときは、webpackChunkName を使いましょう。チャンクのファイル名が決定的になるので、GitLabのバージョンをまたいでブラウザにキャッシュすることができます。

詳細はwebpack のコード分割のドキュメントと vue の動的コンポーネントのドキュメントを参照してください。

ページサイズの最小化

ページサイズを小さくすると、特にモバイルや接続の悪い環境でページの読み込みが速くなります。ページはブラウザによってより迅速に解析され、データプランに上限があるユーザーにとってはより少ないデータしか使用しません。

一般的なヒント

  • 新しいフォントは追加しないでください。
  • 例えば、WOFFよりWOFF2、TTFよりWOFF2が良いでしょう。
  • 可能な限りアセットを圧縮し、最小化しましょう(CSS/JSについては、Sprocketsとwebpackがこれをやってくれます)。
  • 余分なライブラリを追加することなく、合理的に実現できる機能があれば、それを避けましょう。
  • 特定のページでのみ必要なライブラリを読み込むために、上記のようにページ固有のJavaScriptを使用してください。
  • 可能な限りコード分割ダイナミックインポートを使用し、初期に必要のないコードを遅延ロードします。
  • 高性能アニメーション

追加リソース