🚅

WEBアプリケーションのパフォーマンスをUPするために知っておくべき技術と知識

2021/09/07に公開

全てを計測する

パフォーマンスチューニングの最も大事な原則は計測することです。
計測用のツールをまとめておきます。

WEBページの計測ツール

Google PageSpeed Insights
Pingdom
YSlow
Performance Budget Calculator

ネットワーク通信の計測ツール

iddler

https://www.telerik.com/fiddler
ネットワークの内容をキャプチャしてくれるツールです。
https://zappysys.com/blog/how-to-use-fiddler-to-analyze-http-web-requests/

ブラウザの開発者ツール

Fiddlerをインストールしなくてもブラウザの開発者ツールでネットワークの内容をキャプチャすることもできます。Fiddlerの方が高機能な印象ですが簡単に調べるだけならブラウザの開発者ツールでも良いでしょう。

Wireshark

https://www.wireshark.org/download.html
TCP/IP通信のキャプチャができるソフトです。
https://knowledge.sakura.ad.jp/6286/
https://hldc.co.jp/blog/2020/04/15/3988/

アプリケーション開発ではあまりお世話にならないかもしれませんが通信を深く見たい時がいつか来るかもしれません。覚えておきましょう。

他サーバーサイドの計測ツール(ASP.NET, C#)

BenchmarkDotNet

https://benchmarkdotnet.org/articles/overview.html
C#コードの計測をするツールです。Nugetから利用できます。Stopwatchクラスではなくこちらを使いましょう。StopwatchではGCなどの影響を正しく計測できません。

Visual Studio プロファイリング ツール

https://docs.microsoft.com/ja-jp/visualstudio/profiling/?view=vs-2019
Visual Studioのプロファイリングツールを使用するとCPU、メモリ、スレッド、GC、LOHなどの状態を確認できます。呼び出しツリーを見ればどのメソッドのどのコードがCritical Pathなのかすぐに見つけることができます。

正しい優先順位で作業する

上記のツールを元にどこがボトルネックになっているのかを把握してから作業開始です。一番影響が大きいところから優先して作業することになります。まずはサーバー、ネットワーク、クライアントのどこが一番遅いかを特定しましょう。
以下に一般的に優先順位が高そうなものから対策を並べていってみます。

リダイレクトを避ける

不要なリダイレクトは避けましょう。リダイレクトがあると簡単に待ち時間が2倍になります。認証処理などでリダイレクトを連発するような設計はなるだけ避けましょう。

gzipの圧縮を有効にする

圧縮を有効にすることでファイルサイズを大幅に圧縮できます。HTMLファイルにgzip圧縮を使用すると約3分の1にファイルサイズを圧縮できます。100kbのHTMLファイルが33kbになるので大きな効果が期待できます。

HTTPリクエストの数を減らす

HTMLファイルのロード後、画像ファイル・CSSファイル・JavaScriptファイルのダウンロードが走ります。可能であればJavaScript,CSSを1つのファイルにまとめましょう。webpack,gulp,gruntなどのツールで1つのファイルにまとめられます。画像ファイルはCSSスプライトで1つにまとめましょう。HTTPリクエストの数を減らすと劇的にパフォーマンスが向上します。

帯域幅と遅延について理解する

帯域幅と遅延の違いについて理解しましょう。帯域幅は道路の車線数、遅延は道路の距離と考えると理解がしやすいかもしれません。モバイルでは帯域幅が狭くデータのダウンロードに時間がかかります。ファイルサイズ、リクエスト数を優先して改善する必要があるでしょう。

遅延が少ないクラウドを選択する

WEBアプリケーションやモバイルから呼び出すWEB APIはクラウドに配置することになると思います。その際にサービスの利用者の国の場所から近い場所を選ぶことで遅延を少なくすることが可能です。日本で展開するサービスならばアメリカやヨーロッパのデータセンターではなく日本のデータセンタを利用しましょう。

HTTP/2 over SSLを使用する

HHTTP/2を使用すると複数のファイルを1回のHTTPリクエストで送信できるようになります。その効果は劇的です。こちらのサイトで確認してみてください。
http://www.httpvshttps.com/
モダンブラウザは全てHTTP/2をサポートしています。
https://caniuse.com/#feat=http2
サーバーが対応しているならば使用していきましょう。

ファイルサイズを最小化する

CSSやJavaScriptなどのファイルを圧縮しファイルサイズを最小化しましょう。不要な空白文字やJavaScriptの変数名などを圧縮することでファイルサイズを最小化します。

CSSを最初にロードする

CSSを最初にロードしましょう。HTMLのheadタグにCSSファイルの参照を記述しましょう。ページのダウンロード時にブラウザは画面のレンダリング処理を始めます。CSSを途中に記述した場合、新たなセレクタ定義によってレンダリング処理が再度実行されることになります。結果として画面の表示に時間がかかることになります。最初にCSSを全て解析済みであればレンダリング処理を再度実行する必要はなくなり、画面の描画が無駄なく完了することになります。

JavaScriptを最後にロードする

JavaScriptの実行中はレンダリングがブロックされます。ユーザーにとっては画面の描画の完了までが体感するそのページの速さになります。なのでJavaScriptの実行は画面の描画が終わったあとに実行するようにしましょう。

画像を別ドメインに配置する

ブラウザのリクエストの並列処理数はドメインごとで制限があります。2020年現在のモダンブラウザはだいたい5-8くらいのようです。
https://stackoverflow.com/questions/7456325/get-number-of-concurrent-requests-by-browser
多数の画像ファイルがある場合、画像ファイルは別ドメインからダウンロードするようにしましょう。1つ画像用の別ドメインのサイトを作りそこからダウンロードというのが最初の方法になります。

非常に多くの画像がある場合、複数のドメインに画像を分けることも検討しましょう。ただしドメインを複数に分けるということはDNSルックアップについて考慮にいれる必要があります。DNSルックアップは100ミリ秒-150ミリ秒かかります。時にはもっとかかることもあり得ます。原則に立ち返りパフォーマンスを計測し、トレードオフを考慮して落としどころを決める必要があります。

画像を圧縮する

画像ファイルを圧縮しましょう。SVGを使用すると圧倒的に小さいサイズで画像を表示できます。png, jpegファイルの場合、圧縮処理をしてくれるWEBサービスを利用しましょう。
https://tinypng.com/

画像を遅延ロードする

画面に表示されていない画像ファイルについてはロードせず、スクロールで表示されるタイミングで表示すると無駄なダウンロードが減り、表示が高速化します。

TTFB

TTFBはTime to First Byteの略でユーザーに最初の1バイトが到着するまでの時間になります。これはユーザーの体感する速度に近いのでこの速度を最適化することは大事です。TTFBの内訳は以下のようになります。

TTFBを減らし、WordPressのページ読み込み時間を改善する方法について

  1. サーバーへのリクエスト(遅延に注意)
  2. サーバー処理(後述)
  3. クライアントへの送信(クライアントのネットワーク環境)
    TTFBが100ミリ秒未満だと非常に高速に感じます。
    静的なサイトでTTFBが600ミリ秒以上だと少し問題ありです。改善の必要があるでしょう。
    TTFBの改善のためのサーバーサイドの作業について以下に書いていきます。

サーバーサイドのDBアクセスを最適化する

例えば以下のような画面を考えます。

こういった画面を作るには下記の順でfor文などで繰り返し処理を書くことになるでしょう。

複数のユーザー→複数の日付→複数のタスク
概念的にはこんな感じになります。

private void ShowCalendar()
{
List<UserRecord> userList = GetUserList(); //ネットワーク通信その1
foreach (var user in userList)
{
    var startDate = new DateTime(2020, 9, 12);
    for (var i = 0; i < 21; i++)
    {
        var date = startDate.AddDays(i);
        var taskList = GetTaskList(date); //ネットワーク通信その2
        foreach (var task in taskList)
        {
            CreateTaskTableCell(task);
        }
    }
}
}
private void CreateTaskTableCell(TaskRecord record)
{
   //HOT PATH!!!!!!

   // Get color of Task
   var color = GetColorFromDatabase(); //ネットワーク通信その3
}

とこんな感じだとしましょう。ネットワーク通信のコードが3つあります。

CreateTaskTableCellメソッドの内部のコードはホットコードパスになります。このメソッドは非常に多くの回数呼ばれます。例えばユーザーが100人、日数が21日、タスクが1日あたり平均5個ある場合、10500回呼ばれることになります。その中でDBから色データを取得するなどというコード(ネットワーク通信その3)を書いた場合、DBアクセスが10ミリ秒だとすると10ミリ秒×10500=105秒はかかることになります。ページが表示されるまで2分弱かかることになるでしょう。ネットワーク通信その2は2100回なのでネットワーク通信その3よりはましですがこれも改善の必要があります。

ネットワーク通信その1は1回しか呼ばれていないのでそのままでも良いでしょう。

DataSetよりもDataReaderを使用する

C#のDataSetやDataTableはいろいろな機能があって遅いです。単にデータを読み取るだけならばDataReaderを使用するようにしましょう。

Table-Valued ParameterかSqlBulkCopyを使用して複数行を処理する

複数行を一括で処理する場合はTVP(Table-Valued Parameter)かSqlBulkCopyを使用しましょう。
https://www.sentryone.com/blog/sqlbulkcopy-vs-table-valued-parameters-bulk-loading-data-into-sql-server
1000以下のレコードであればTVPの方が高速です。これはBulk Insertは初期化処理に多少時間がかかるためです。
https://docs.microsoft.com/en-us/sql/relational-databases/tables/use-table-valued-parameters-database-engine?view=sql-server-ver15#BulkInsert
1000以上のレコードの挿入の場合、SqlBulkCopyを使用してデータを挿入しましょう。

TVPのストアドをC#から呼び出すのは結構面倒くさいです。DbSharpを使用するとC#の呼び出しコードを簡単に自動生成できます。
https://tech.tinybetter.com/Article/c8c7085c-3718-99b0-ca9d-39fea40311b7/View
活用しましょう。

サーバー処理を非同期で実行する

C#ではasyncという非同期実行の仕組みがあります。処理の待ちを非同期にすることでスレッドを解放し、より多くのリクエストをさばけるようになります。積極的に活用しましょう。

サーバーでキャッシュを活用する

メモリキャッシュ

サーバーのメモリ上によく使うデータをキャッシュして再利用します。マスタデータなどをキャッシュすることでDBアクセスを無くしクライアントにより高速にレスポンスを返すことができます。

Redisキャッシュ

サーバーのメモリの代わりにRedisサーバーのメモリ上にキャッシュします。DBだとハードディスクアクセスになりますがRedisだとメモリアクセスになるのでDBよりは高速です。

ページをキャッシュする

過去データを表示するHTMLなどをキャッシュすることでレスポンスを高速化します。例えば先月の売上データの表を表示するページなどは既に確定して変更がないデータのため、キャッシュしても良いでしょう。逆に変更が多数あるページ、例えば最新コメント一覧ページなどはHTMLキャッシュには向かないでしょう。

グラフ用のデータをキャッシュする

クライアントで例えばGoogleChartsの画像を表示する際に、サーバーで元データを用意する必要があります。過去データで変更が無い場合(例えば先月の売上データなど)は元データをキャッシュしておくとグラフを高速に表示できるでしょう。

ブラウザキャッシュを活用する

ブラウザにはキャッシュ機能があります。
https://satoyan419.com/browser-caching/
CSSやJavaScriptなどあまり変更がされないファイルはブラウザキャッシュを使用することでHTTPリクエストを減らすことができパフォーマンスの向上が可能です。

リクエストレベルキャッシュ

AJAXの結果をキャッシュすることでページの表示を高速化できます。例えば昨日の人気記事一覧の部分HTMLをキャッシュすることでサーバーへのリクエストを無しにして表示を高速化できます。

CDNを使用する

Content Delivery Networkを使用して画像やZIPファイルなどをダウンロードすることで遅延の低減とサーバーの負荷の減少を実現できます。
https://blog.redbox.ne.jp/what-is-cdn.html

Webフォントについて

WEBフォントを使用するとサイトのデザインが洗練された感じになりますがパフォーマンスは低下します。特に日本語フォントは漢字などがあるためにサイズが大きいです。

PreRender,Prefetch,PreConnectを使用する

ユーザーの次の行動に応じて事前に準備をすることでパフォーマンスを向上させることができます。

PreRenderを使用する

多くのユーザーが次に表示するページが分かっている場合、PreRenderを使用することで表示速度を劇的に向上させることが可能です。例えばアンケートの回答ページで20つの質問が4つのページで構成されている場合などに利用できます。

利用すると予測されるリソース(画像,CSS,JavaScriptなど)をあらかじめダウンロードすることでパフォーマンスを向上させます。

<link rel=”prefetch” href=”//example.com/next-page.html” as=”html” crossorigin=”use-credentials”>
<link rel=”prefetch” href=”/library.js” as=”script”>

DNS-prefetching

DNSルックアップは非常に遅い処理です。DNSルックアップをあらかじめやっておくことでパフォーマンスが向上します。

<link rel=”dns-prefetch” href=”//img.mysite.com”>

PreConnect

DNSルックアップに加えてTCPハンドシェイクとTSLネゴシエーションまで完了させます。

<link rel=”preconnect” href=”//img.example.com”>
<link rel=”preconnect” href=”//cdn.mysite.com” crossorigin>

404エラーを避ける

例外の発生はサーバーに負荷がかかります。サーバーのリソースが不足するとレスポンスが遅くなります。不要な404は発生させないようにしましょう。

高いサーバーを買う

最後の方法は「お金にモノを言わせて高いサーバーを購入」します。時間がない、人がいない、しかしお金はある!といった場合に非常に有効です。

まとめ

この記事では具体的な方法についてはあまり書いていませんが、この記事のキーワードでGoogleなどで検索するとたくさん記事が出てきます。目次として利用いただければ幸いです。

一般的なコンピューターサイエンスの知識とパフォーマンス向上についてはこちらにまとめておりますので是非参考にしてみてください↓
https://tech.tinybetter.com/Article/fc5f8084-c0aa-250b-826f-39fea40e48ec/View

Discussion