Railsは高速です:ビューのパフォーマンスを最適化する
この投稿では、Railsビューのパフォーマンスを改善するための実証済みの実際の方法について説明します。具体的には、データベースの効率、ビューの操作、およびキャッシュに焦点を当てます。
「時期尚早の最適化はすべての悪の根源である」というフレーズは、文脈から少し外れていると思います。単純な最適化手法が指摘されている場合、開発者がコードレビュー中にこれを使用するのをよく耳にします。有名な「動作させてから最適化する」、つまりテストしてからデバッグしてから、もう一度テストするなどのことをご存知でしょう。
ありがたいことに、コードを書き始めた瞬間から使用できる、シンプルで効果的なパフォーマンスと最適化の手法がいくつかあります。
👋この記事が気に入った場合は、Rubyパフォーマンス監視チェックリストにある他のRuby(on Rails)パフォーマンス記事をご覧ください。
投稿全体を通して、基本的なRailsアプリに固執し、それを改善して結果を比較します。
基本的なRailsアプリには次のモデルがあります:
-
人(住所が多い)
- name:string
- votes_count:integer
-
プロファイル(個人に属する)
- address:string
Personモデルは次のようになります:
# == Schema Information
#
# Table name: people
#
# id :integer not null, primary key
# name :string
# votes_count :integer
# created_at :datetime not null
# updated_at :datetime not null
#
class Person < ApplicationRecord
# Relationships
has_many :profiles
# Validations
validates_presence_of :name
validates_uniqueness_of :name
def vote!
update votes_count: votes_count + 1
end
end
これは、プロファイルモデルのコードです:
# == Schema Information
#
# Table name: profiles
#
# id :integer not null, primary key
# address :text
# person_id :integer
# created_at :datetime not null
# updated_at :datetime not null
#
class Profile < ApplicationRecord
# Relationships
belongs_to :person
# Validations
validates_presence_of :address
end
1000人を取り込むためのシードファイルもあります。 Faker gemを利用することで、これを簡単に行うことができます。
次に、ApplicationControllerに「home」というアクションを作成します。
def home
@people = Person.all
end
home.html.erbのコードは次のとおりです。
<ul>
<% @people.each do |person| %>
<li id="<%= person.id %>"><%= render person %></li>
<% end %>
</ul>
ドライランを実行して、これに対するページのパフォーマンスを測定してみましょう。
そのページの読み込みにはなんと1066.7msかかりました。良くない!これが私たちが削減を目指すものです。
データベースクエリ
パフォーマンスの高いアプリケーションを構築するための最初のステップは、リソースの使用率を最大化することです。ほとんどのRailsアプリは、データベースからビューに何かをレンダリングするので、最初にデータベース呼び出しを最適化してみましょう!
このデモンストレーションでは、MySQLデータベースを使用します。
1066msの初期負荷がどのように崩壊するかを見てみましょう。
414.7「controllers/application_controller#home」を実行するには
...
(0.1ms) SELECT "profiles"."address" FROM "profiles" WHERE "profiles"."person_id" = ? [["person_id", 996]]
Rendered people/_person.html.erb (1.5ms)
(0.2ms) SELECT "profiles"."address" FROM "profiles" WHERE "profiles"."person_id" = ? [["person_id", 997]]
Rendered people/_person.html.erb (2.3ms)
(0.1ms) SELECT "profiles"."address" FROM "profiles" WHERE "profiles"."person_id" = ? [["person_id", 998]]
Rendered people/_person.html.erb (2.1ms)
(0.2ms) SELECT "profiles"."address" FROM "profiles" WHERE "profiles"."person_id" = ? [["person_id", 999]]
Rendered people/_person.html.erb (2.3ms)
(0.2ms) SELECT "profiles"."address" FROM "profiles" WHERE "profiles"."person_id" = ? [["person_id", 1000]]
Rendered people/_person.html.erb (2.0ms)
Rendered application/home.html.erb within layouts/application (890.5ms)
Completed 200 OK in 1066ms (Views: 890.5ms | ActiveRecord: 175.4ms)
「application/home.html.erb」および「people/_person.html.erb」パーシャルをレンダリングするための519.2および132.8。
何か変なことに気づきましたか?
コントローラで1回のデータベース呼び出しを行いましたが、すべてのパーシャルが独自のデータベース呼び出しも行います。 N+1クエリの問題を紹介します。
1。 N+1クエリ
これは非常に人気のある単純な最適化手法ですが、この間違いが非常に蔓延しているため、最初に言及する価値があります。
「people/_person.html.erb」の機能を見てみましょう:
<ul>
<li>
Name: <%= person.name %>
</li>
<li>
Addresses:
<ul>
<% person.profiles.each do |profile| %>
<li><%= profile.address %></li>
<% end %>
</ul>
</li>
</ul>
<%= button_to "Vote #{person.votes_count}", vote_person_path(person) %>
基本的に、それはその人のプロファイルをデータベースに照会し、それぞれをレンダリングします。つまり、N個のクエリ(Nは人数)と、コントローラーで実行した1個のクエリ(つまり、N + 1)を実行します。
これを最適化するには、MySQLデータベース結合を利用し、RailsActiveRecordには関数が含まれています。
以下に一致するようにコントローラーを変更しましょう:
def home
@people = Person.all.includes(:profiles)
end
すべての人は1つのMySQLクエリによってロードされ、それぞれのクエリはすべて別のクエリにロードされます。 N+1をたった2つのクエリにもたらします。
これによりパフォーマンスがどのように向上するかを見てみましょう!
ページの読み込みにかかった時間はわずか936msでした。以下では、「application_controller#home」アクションが2つのMySQLクエリを実行することがわかります。
Rendered people/_person.html.erb (0.3ms)
Rendered people/_person.html.erb (0.2ms)
Rendered people/_person.html.erb (0.3ms)
Rendered people/_person.html.erb (0.3ms)
Rendered people/_person.html.erb (0.3ms)
Rendered people/_person.html.erb (0.3ms)
Rendered people/_person.html.erb (0.3ms)
Rendered people/_person.html.erb (0.2ms)
Rendered application/home.html.erb within layouts/application (936.0ms)
Completed 200 OK in 936ms (Views: 927.1ms | ActiveRecord: 9.3ms)
2。使用するものだけをロードする
これがホームページの外観です。
住所だけが必要で、他には何も必要ないことがわかります。ただし、「_ person.html.erb」パーシャルでは、プロファイルオブジェクトをロードします。その変更を行う方法を見てみましょう。
<li>
Addresses:
<ul>
<% person.profiles.pluck(:address).each do |address| %>
<li><%= address %></li>
<% end %>
</ul>
</li>
N + 1クエリの詳細については、ActiveRecordのパフォーマンス:N+1クエリのアンチパターンをご覧ください。
ヒント:このスコープを作成して、「models/profile.rb」ファイルに追加できます。ビューファイル内の生のデータベースクエリはあまり役に立ちません。
3。すべてのデータベース呼び出しをコントローラーに移動する
たとえば、この架空のアプリケーションの将来で、ホームページにユーザーの総数を表示したいとします。
単純!次のようなビューで電話をかけましょう:
# of People: <%= @people.count %>
わかりました、それは十分に簡単です。
別の要件があります。ページの進行状況を表示するUI要素を作成する必要があります。ページの人数を合計数で割ってみましょう。
Progress: <%= index / @people.count %>
残念ながら、同僚はあなたがすでにこのクエリを実行したことを知らず、ビューで何度も何度もクエリを実行します。
コントローラは次のようになっていますか:
def home
@people = Person.all.includes(:profiles)
@people_count = @people.count
end
すでに計算された変数を再利用する方が簡単だったでしょう。
これはページの読み込み速度の直接的な向上には寄与しませんが、さまざまなビューページからのデータベースへの複数の呼び出しを防ぎ、キャッシュなど、後で実行できる最適化の準備に役立ちます。
4。できる限りパジネート!
必要なものだけをロードするのと同じように、必要なものだけを表示するのにも役立ちます。ページ付けを使用すると、ビューは情報の一部をレンダリングし、残りをオンデマンドでロードし続けます。これにより、ミリ秒が大幅に短縮されます。 will_paginateとkaminariの宝石は、これを数分で実行します。
これが引き起こす1つの厄介な点は、ユーザーが「次のページ」をクリックし続ける必要があることです。そのために、「無限スクロール」を見て、ユーザーのエクスペリエンスを大幅に向上させることもできます。
HTMLの再読み込みの回避
従来のRailsアプリでは、HTMLビューのレンダリングに多くの時間がかかります。幸い、これを減らすために実行できる対策があります。
1。ターボリンク
これは、標準のRailsアプリにまとめられています。 TurbolinksはJavaScriptライブラリであり、どこでも機能し(静的ページのように、Railsがなくても)、サポートされていないブラウザでは正常に機能しなくなります。
すべてのリンクをAJAXリクエストに変換し、JSを介してページの本文全体を置き換えます。これにより、CSS、JS、画像をリロードする必要がなくなるため、パフォーマンスが大幅に向上します。
ただし、カスタムJSを作成する場合は、「TurbolinkssafeJS」を作成するために特別な注意を払う必要があります。詳しくはこちらをご覧ください。
2。 AJAXリクエストを使用する
Turbolinksと同じように、リンクとボタンの一部をAJAXリクエストに変換することもできます。ここでの違いは、Turbolinksのように本文全体を置き換えるのではなく、HTMLを置き換えるものを制御できることです。
AJAXの動作を見てみましょう!
サンプルアプリには、ユーザーごとに「投票」ボタンがあります。そのアクションを実行するのにかかる時間を測定しましょう。
Started POST "/people/1/vote" for 127.0.0.1 at 2020-01-21 14:50:49 +0530
Processing by PeopleController#vote as HTML
Person Load (0.3ms) SELECT "people".* FROM "people" WHERE "people"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.1ms) begin transaction
Person Exists (4.5ms) SELECT 1 AS one FROM "people" WHERE "people"."name" = ? AND ("people"."id" != ?) LIMIT ? [["name", "Deon Waelchi"], ["id", 1], ["LIMIT", 1]]
SQL (1.0ms) UPDATE "people" SET "votes_count" = ?, "updated_at" = ? WHERE "people"."id" = ? [["votes_count", 1], ["updated_at", "2020-01-21 09:20:49.941928"], ["id", 1]]
Redirected to https://localhost:3000/
Completed 302 Found in 24ms (ActiveRecord: 7.5ms)
Started GET "/" for 127.0.0.1 at 2020-01-21 14:50:49 +0530
Processing by ApplicationController#home as HTML
Rendering application/home.html.erb within layouts/application
Rendered people/_person.html.erb (2.4ms)
(0.3ms) SELECT "profiles"."address" FROM "profiles" WHERE "profiles"."person_id" = ? [["person_id", 30]]
Rendered people/_person.html.erb (2.2ms)
...
Rendered application/home.html.erb within layouts/application (159.8ms)
Completed 200 OK in 190ms (Views: 179.0ms | ActiveRecord: 6.8ms)
これには、ページの再読み込みと同じ時間がかかり、さらに実際の投票部分に少し余分な時間がかかりました。
それをAJAXリクエストにしましょう。これで、「people/_person.html.erb」は次のようになります。
<%= button_to "Vote #{person.votes_count}", vote_person_path(person), remote: true %>
コントローラアクションは、次のようなJS応答を返します。
$("#<%= @person.id %>").html("<%= j render(partial: 'person', locals: {person: @person}) %>");
ご覧のとおり、必要なコンテンツのみを置き換えています。 divにフックして置き換えるためのHTMLIDを提供します。もちろん、ボタンのコンテンツのみを置き換えることでこれをさらに最適化できますが、この投稿の目的のために、部分全体を置き換えましょう。
結果?
Started POST "/people/1/vote" for 127.0.0.1 at 2020-01-21 14:52:56 +0530
Processing by PeopleController#vote as JS
Person Load (0.2ms) SELECT "people".* FROM "people" WHERE "people"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.1ms) begin transaction
Person Exists (0.3ms) SELECT 1 AS one FROM "people" WHERE "people"."name" = ? AND ("people"."id" != ?) LIMIT ? [["name", "Deon Waelchi"], ["id", 1], ["LIMIT", 1]]
SQL (0.4ms) UPDATE "people" SET "votes_count" = ?, "updated_at" = ? WHERE "people"."id" = ? [["votes_count", 2], ["updated_at", "2020-01-21 09:22:56.532281"], ["id", 1]]
(1.6ms) commit transaction
Rendering people/vote.js.erb
(0.2ms) SELECT "profiles"."address" FROM "profiles" WHERE "profiles"."person_id" = ? [["person_id", 1]]
Rendered people/_person.html.erb (3.2ms)
Rendered people/vote.js.erb (6.3ms)
Completed 200 OK in 31ms (Views: 14.6ms | ActiveRecord: 2.9ms)
30ms!それでおしまい!それはどれくらい素晴らしいですか?
ProTip:HTML IDやクラスをいじって、いつ/何を置き換えるかを考えたくない場合は、render_asyncgemの使用を検討してください。それは箱から出して多くの重労働を行います。
3。 Websocketを使用する
HTMLリロードの優れた点の1つは、サーバーから毎回新鮮なコンテンツを取得できることです。 AJAXリクエストでは、小さなスニペットの最新のコンテンツのみが表示されます。
WebSocketは、クライアントが新しい情報を要求するのではなく、サーバーが更新をクライアントにプッシュできるようにする優れたテクノロジーです。
これは、動的なWebページを作成する必要がある場合に役立ちます。あなたのウェブサイトにゲームのスコアを表示する必要があると想像してください。新しいコンテンツを取得するには、
- ページ全体をリロードするようにユーザーに指示する
- スコアだけを更新するリロードボタンを提供します
- JavaScriptを使用して、毎秒バックエンドをポーリングし続けます
- これにより、データに変更がない場合でもサーバーにpingが送信され続けます
- 各クライアントは毎秒電話をかけます-サーバーを簡単に圧倒します
- WebSocketを使用してください!
WebSocketを使用すると、サーバーはデータをすべてのクライアント(またはサブセット)にプッシュするタイミングを制御できます。サーバーはデータがいつ変更されるかを知っているため、変更があった場合にのみデータをプッシュできます!
Rails 5はActionCableをリリースしました。これにより、WebSocketのすべてを管理できます。これは、クライアントがサーバーにサブスクライブするためのJSフレームワークと、サーバーが変更を公開するためのバックエンドフレームワークを提供します。アクションケーブルを使用すると、任意のWebSocketサービスを選択できます。自己管理型のWebソケットサービスであるFaye、またはサブスクリプションサービスであるPusherの可能性があります。
個人的には、管理する必要のあるものの数が減るので、このサブスクリプションを選択します。
さて、WebSocketに戻りましょう。 ActionCableの設定が完了すると、ビューはサーバーからのJSON入力をリッスンできなくなります。受信すると、作成したフックアクションがそれぞれのHTMLコンテンツに置き換わります。
RailsのドキュメントとPusherには、WebSocketを使用してビルドする方法に関する優れたチュートリアルがあります。必読です!
キャッシング
ロード時間の大部分は、ビューのレンダリングで使い果たされます。これには、すべてのCSS、JS、画像の読み込み、ERBファイルからのHTMLのレンダリングなどが含まれます。
読み込み時間のチャンクを減らす1つの方法は、一定の時間またはイベントが発生するまで静的なままであることがわかっているアプリケーションの部分を特定することです。
この例では、誰かが投票するまで、ホームページは基本的にすべての人にとって同じように見えることは明らかです(現在、ユーザーが自分のアドレスを編集するオプションはありません)。イベント(投票)が発生するまで、「home.html.erb」ページ全体をキャッシュしてみましょう。
ダリの宝石を使ってみましょう。これは、Memcachedを使用して、情報のフラグメントをすばやく保存および取得します。 Memcachedにはストレージ用のデータ型がないため、基本的に好きなものを保存できます。
1。ビューのキャッシュ
キャッシュなしの2000レコードのロード時間は、3500ミリ秒です!
すべてを「home.html.erb」にキャッシュしましょう。簡単です
<% cache do %>
<ul>
<% @people.each do |person| %>
<li id="<%= person.id %>"><%= render person %></li>
<% end %>
</ul>
<% end %>
次に、Dalli gemをインストールし、「development.rb」のキャッシュストアを次のように変更します。
config.cache_store = :dalli_store
次に、MacまたはLinuxを使用している場合は、次のようにMemcachedサービスを開始します。
memcached -vv
リロードしましょう!!
約537msかかりました!これは速度が7倍向上しています!
また、HTML全体がMemcachedに保存され、データベースにpingを実行することなく、そこから再度読み取られるため、MySQLクエリがはるかに少ないことがわかります。
アプリケーションログにアクセスすると、このページ全体がキャッシュから読み取られたことがわかります。
もちろん、この例はビューキャッシングの表面を引っ掻いているだけです。部分的なレンダリングをキャッシュして、それを各人物オブジェクトにスコープする(これはフラグメントキャッシングと呼ばれます)か、コレクション全体をキャッシュする(これはコレクションキャッシングと呼ばれます)ことができます。さらにネストされたビューのレンダリングについては、ロシアの人形のキャッシュを実行できます。
2。データベースクエリのキャッシュ
ビュー速度を向上させるために実行できるもう1つの最適化は、複雑なデータベースクエリをキャッシュすることです。アプリケーションに統計と分析が表示されている場合は、複雑なデータベースクエリを実行して各メトリックを計算している可能性があります。その出力をMemcachedに保存してから、タイムアウトを割り当てることができます。これは、タイムアウト後、計算が再度実行され、キャッシュに保存されることを意味します。
たとえば、アプリケーションがユーザーチームのサイズを表示する必要があると仮定します。これは、直属の部下、外部委託コンサルタントなどの数を含む複雑な計算になる可能性があります。
計算を何度も繰り返す代わりに、キャッシュすることができます!
def team_size
Rails.cache.fetch(:team_size, expires_in: 8.hour) do
analytics_client = AnalyticsClient.query!(self)
analytics_client.team_size
end
end
このキャッシュは8時間後に自動的に期限切れになります。それが発生すると、計算が再度実行され、最新の値が次の8時間キャッシュされます。
3。データベースインデックス
インデックスを使用してクエリを高速化することもできます。人のすべての住所を取得するための簡単なクエリ
person.addresses
このクエリは、Addressテーブルにperson_id
のすべてのアドレスを返すように要求します。 列はperson.id
です 。インデックスがない場合、データベースは各行を個別に検査して、person.id
と一致するかどうかを確認する必要があります。 。ただし、インデックスを使用すると、データベースには特定のperson.id
に一致するアドレスのリストがあります。 。
データベースインデックスの詳細については、こちらの優れたリソースをご覧ください。
概要
この投稿では、データベースの使用率を改善し、サードパーティのツールとサービスを使用し、ユーザーに表示されるものを制限することで、Railsアプリのビューのパフォーマンスを改善する方法について説明しました。
アプリのパフォーマンスを向上させたい場合は、簡単に始めて、測定を続けてください。データベースクエリをクリーンアップしてから、可能な限りAJAXリクエストを作成し、最後にできるだけ多くのビューをキャッシュします。その後、WebSocketとデータベースキャッシングに進むことができます。
ただし、注意が必要です。最適化は滑りやすい坂道です。あなたは私と同じくらい中毒になっているかもしれません!
P.S。本番環境でのRailsアプリのパフォーマンスを監視するには、Ruby開発者がRuby開発者向けに構築したAppSignalのAPMを確認してください。 🚀
-
パフォーマンスを向上させるために Mac を最適化する方法
「初めて iMac を購入したときは、スムーズで完璧なパフォーマンスで最高でした。しかし、今は少し遅くなったと感じており、応答時間は以前のものではありません。」 – デビッド・モリソン 「インターネット サーフィンは、もはや Mac での体験とは異なります。システムの起動も遅くなり、必要なアプリの起動も遅れます。」 – カミラ・スミス. 時間の経過とともに Mac が遅くなったと感じたことはありますか? Mac のパフォーマンスに満足していませんか? Mac を最初に購入したときのほうがよかったと思いますか? あなたのスローマックの犯人は誰? 同じ経験があるなら、Mac を最適化す
-
YouTube アナリティクス:指標を理解し、動画のパフォーマンスを最適化する
誰があなたの YouTube チャンネルを見ているのか気になりませんか?または、どの動画が最高のパフォーマンスを発揮していますか? YouTube アナリティクスを使用すると、視聴者がどこにいるか、獲得/喪失したチャンネル登録者数、コメント、シェア、好き嫌い、視聴時間などを把握できます。このデータを知ることは、公開すべきコンテンツの種類を理解するのに役立ち、動画戦略を最適化してブランドを効果的に成長させる方法を学ぶことができます. 組み込みの YouTube アナリティクスは、動画とチャンネルのパフォーマンスを監視するための集計データと指標を提供します。それでは、YouTube チャンネル