เราเคยดู fragment caching ใน Rails บน AppSignal Academy มาแล้ว นี่ปรับปรุง performance ของ views อย่างมากโดย caching ชิ้นเล็กๆ ของมัน เมื่อ caching partials เรายังได้ benefit เพิ่มเติมคือ reuse ได้ที่อื่นใน views ด้วย cost ต่ำ
สิ่งนี้ทำงานดีสำหรับ small collections แต่ปัญหาจะขึ้นทันทีบน larger collections ในบทความนี้ เราจะดูว่า Rails collection caching ทำงานอย่างไรและเราสามารถใช้มัน speed up rendering ของ large collection ได้อย่างไร
👋 และถ้าคุณชอบบทความนี้ เรามีบทความเกี่ยวกับ Ruby (on Rails) performance มากมาย ดู checklist การ monitor Ruby performance ของเราได้
Rendering a Collection
เริ่มด้วย controller เล็กๆ ที่ load last 100 posts สำหรับ blog index page
class PostsController < ApplicationController def index @posts = Post.all.order(:created_at => :desc).limit(100) end endเพื่อ render posts ใน view เรา loop over instance variable @posts
Posts
<% @posts.each do |post| %> <%= post.body %> <% end %> เมื่อ request หน้านี้ เราเห็น posts ถูก fetch จาก database และ view ถูก render ด้วย 32 milliseconds ใน view layer หน้านี้ค่อนข้างเร็ว
Started GET "/posts" Processing by PostsController#index as HTML Rendering posts/index.html.erb within layouts/application Post Load (1.5ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]] ↳ app/views/posts/index.html.erb:4 Rendered posts/index.html.erb within layouts/application (19.4ms) Completed 200 OK in 37ms (Views: 32.4ms | ActiveRecord: 2.7ms) Rendering a Collection with Partials
ต่อไป เราอยากใช้ post element ใน view อื่น ดังนั้นเราย้าย post HTML ไป partial
Posts
<% @posts.each do |post| %> <%= render post %> <% end %> <%= post.title %>
<%= post.author %> <%= post.body %> Started GET "/posts" Processing by PostsController#index as HTML Rendering posts/index.html.erb within layouts/application Post Load (1.2ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]] ↳ app/views/posts/index.html.erb:4 ... Rendered posts/_post.html.erb (0.1ms) Rendered posts/_post.html.erb (0.1ms) Rendered posts/index.html.erb within layouts/application (205.4ms) Completed 200 OK in 217ms (Views: 213.8ms | ActiveRecord: 1.7ms) ด้วย 213 milliseconds บน view layer คุณเห็นว่า render time เพิ่มขึ้นมาก นี่เพราะ file ใหม่ (partial) ต้อง load, compile และ render สำหรับทุก post มาดูสั้นๆ ว่าเราสามารถปรับปรุง render time ด้วย fragment caching ได้อย่างไร
Fragment Caching
ตามที่อธิบายใน fragment caching article เราจะใช้ cache helper ใน view รอบ render call ด้วยวิธีนี้ เราจะ cache rendering ของ partial สำหรับทุก post
Posts
<% @posts.each do |post| %> <%= cache post do %> <%= render post %> <% end %> <% end %> Started GET "/posts" Processing by PostsController#index as HTML Rendering posts/index_with_partial_caching.html.erb within layouts/application Post Load (1.4ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]] ↳ app/views/posts/index.html.erb:4 ... Read fragment views/posts/index.1ms) Rendered posts/_post.html.erb (0.1ms) Write fragment views/posts/index.1ms) Read fragment views/posts/index.5ms) Rendered posts/_post.html.erb (0.1ms) Write fragment views/posts/index.1ms) Rendered posts/index.html.erb within layouts/application (274.5ms) Completed 200 OK in 286ms (Views: 281.4ms | ActiveRecord: 2.4ms) Request แรกไม่ได้เร็วกว่ามากนัก เพราะยังต้อง render ทุก partial ครั้งแรกและเก็บใน cache store
Started GET "/posts" Processing by PostsController#index as HTML Rendering posts/index.html.erb within layouts/application Post Load (2.2ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]] ↳ app/views/posts/index.html.erb:4 ... Read fragment views/posts/index.1ms) Read fragment views/posts/index.1ms) Rendered posts/index.html.erb within layouts/application (63.8ms) Completed 200 OK in 78ms (Views: 75.5ms | ActiveRecord: 2.2ms) ใน requests ต่อไป เราเห็น time ใน view ลดลงมาก - จาก 286 milliseconds เหลือ 78 milliseconds แต่มันยังช้ากว่า original code เกือบสองเท่า
Note: ถ้าคุณไม่เห็น lines "Read/Write fragment" ใน logs ให้ enable fragment cache logging ใน development environment ซึ่งเป็น false โดยค่าเริ่มต้นบน Rails 5.1+:
# config/environments/development.rb config.action_controller.enable_fragment_cache_logging = true Collection Caching
ใน Rails 5 มีการทำงานมากเพื่อทำให้ collection caching เร็วขึ้น เพื่อ leverage improvements เหล่านี้ เราต้องเปลี่ยน view code แทนที่จะเรียก cache helper เอง เราสามารถขอให้ Rails render entire collection และ cache พร้อมกันได้
Posts
<%= render partial: :post, collection: @posts, cached: true %> โปรดสังเกต render @collection, cached: true shorthand ไม่ได้ work สำหรับ caching speed improvement นี้
Started GET "/posts" Processing by PostsController#index as HTML Rendering posts/index.html.erb within layouts/application Post Load (1.4ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]] ↳ app/views/posts/index.html.erb:4 Rendered collection of posts/_post.html.erb [0 / 100 cache hits] (28.2ms) Rendered posts/index.html.erb within layouts/application (46.6ms) Completed 200 OK in 64ms (Views: 59.9ms | ActiveRecord: 2.0ms) ใน request แรก เราเห็น improvement ใหญ่ใน time บน view layer เพราะ Rails ตอนนี้ prepare partial ที่ใช้สำหรับ entire collection ล่วงหน้า แทนที่จะทำแต่ละ post แยกกัน
Started GET "/posts" Processing by PostsController#index as HTML Rendering posts/index.html.erb within layouts/application Post Load (1.3ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]] ↳ app/views/posts/index.html.erb:4 Rendered collection of posts/_post.html.erb [100 / 100 cache hits] (19.2ms) Rendered posts/index.html.erb within layouts/application (26.5ms) Completed 200 OK in 37ms (Views: 35.7ms | ActiveRecord: 1.3ms) ใน requests ต่อไป เราเห็น improvement อีก - จาก 64 milliseconds เหลือประมาณ 35 milliseconds Speed improvement ใหญ่สำหรับ entire collection ทำได้โดย Rails optimization สำหรับ collections แทนที่จะ check cache availability สำหรับทุก partial Rails check ทุก cache keys ของ collection พร้อมกัน ประหยัดเวลา query cache store
Added benefit ของ caching helper นี้คือ summarized logging ของ collection ใน request แรก ไม่มี cache keys หาเจอ [0 / 100 cache hits] แต่ใน request ที่สอง หาเจอทั้งหมด [100 / 100 cache hits]
หลัง update บาง objects ใน database เรายังเห็นว่ามี keys กี่ตัวที่ stale
Rendered collection of posts/_post.html.erb [88 / 100 cache hits] (13.4ms) มี speed improvement มากที่จะได้ด้วย optimized collection rendering และ caching นี้ ความแตกต่างจะใหญ่ขึ้นอีกเมื่อ render larger collections เว้นแต่คุณต้องการ customized views สำหรับ collection strategy ที่ optimized นี้คือทางไปสำหรับ Rails apps ของคุณ ที่ AppSignal เรา speed up admin view ที่ render thousands of records อย่างมากด้วยวิธีนี้
มีคำถามเกี่ยวกับ caching collections ใน Rails? อย่าลังเลบอกเราที่ @AppSignal! ถ้ามีความคิดเห็นเกี่ยวกับบทความหรือ topics ที่อยากให้เรา cover โปรดติดต่อเรา