เมื่อ Rails application รับ request controller ปกติจะขอข้อมูลจาก model Model จึงดึงจาก database ส่งกลับ controller Controller สุดท้าย render view แสดงข้อมูลในรูปแบบมนุษย์อ่านได้
ในบางสถานการณ์ การ render view เป็น expensive operation โดยเฉพาะถ้า view ต้องแสดงข้อมูลเยอะ เช่นแสดงรายการสินค้าทั้งหมดในร้านค้า ในกรณีแบบนั้น caching ส่วนของ returned view ช่วยเร่งสิ่งต่างๆ โดยเฉพาะเมื่อข้อมูลไม่เปลี่ยนบ่อย
👋 หากชอบบทความนี้ เรามีเนื้อหาเกี่ยวกับประสิทธิภาพ Ruby (on Rails) มากมาย ดู checklist การติดตามประสิทธิภาพ Ruby ของเราได้เลย
ทดสอบ Caching ใน Local
Caching ปิดใน development โดยค่าเริ่มต้น เพื่อให้คุณได้ fresh responses จาก app เสมอ เพื่อทดสอบ caching locally คุณต้องเปิดใน development config
ใน Rails 5 คุณเปิด caching ชั่วคราวจาก command line ได้ จะใช้ memory store หมายถึง cached fragments จะอยู่ในหน่วยความจำใน Ruby process ของ web server
$ rails dev:cache
Development mode is now being cached.
รันคำสั่งเดิมอีกครั้งเพื่อปิด caching ได้
Fragment Caching
สมมติเรามีหน้าที่แสดงสินค้าทั้งหมดในร้านค้าหน้าเดียว ทำได้โดย index view ที่แสดงสินค้า
# app/views/products/index.html.erb
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Image url</th>
<th>Price</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @products.each do |product| %>
<%= render product %>
<% end %>
</tbody>
</table>
สำหรับแต่ละสินค้า _product.html.erb partial จะ render แสดง table row พร้อมรายละเอียดสินค้า
# app/views/products/_product.html.erb
<tr>
<td><%= product.title %></td>
<td><%= product.description %></td>
<td><%= product.image_url %></td>
<td><%= product.price %></td>
<td><%= link_to 'Show', product %></td>
<td><%= link_to 'Edit', edit_product_path(product) %></td>
<td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
Request หน้านี้ locally ใน development mode พร้อม 25 products ใน database ใช้เวลาประมาณ 300 milliseconds เกือบเวลาทั้งหมดใช้ render partials
Response อาจเร็วกว่าใน production เพราะ bundled assets, logging น้อยกว่า และ web servers เร็วกว่า แม้ตัวเลขใน development ไม่ accurate แต่จะแสดงว่าส่วนไหนของ request ช้าที่สุด เพื่อลองเร่งมัน
Started GET "/products" for ::1 at 2018-03-13 12:16:08 +0100
Processing by ProductsController#index as HTML
Rendering products/index.html.erb within layouts/application
Product Load (0.4ms) SELECT "products".* FROM "products"
Rendered products/_product.html.erb (1.4ms)
Rendered products/_product.html.erb (0.4ms)
...
Rendered products/index.html.erb within layouts/application (257.5ms)
Completed 200 OK in 295ms (Views: 290.4ms | ActiveRecord: 0.4ms)
เพื่อประหยัดเวลา render partials ทั้งหมด เราใช้ fragment caching built-in ของ Rails ซึ่งเก็บส่วนของ rendered view เป็น fragment สำหรับ subsequent requests pre-saved fragment จะถูกใช้แทนการ render ใหม่
เพื่อ cache fragment ให้ wrap ใน block ด้วย cache helper
<table>
...
<tbody>
<% @products.each do |product| %>
<% cache(product) do %>
<%= render product %>
<% end %>
<% end %>
</tbody>
</table>
เพื่อดูว่าการ cache products เร่ง response ได้หรือไม่ เรา request หน้านี้ 2 ครั้ง Request ที่สองควรเร็วกว่ามาก เพราะแต่ละ product ใน view pre-rendered และเก็บใน cache แล้ว
Started GET "/products" for ::1 at 2018-03-13 12:17:29 +0100
Processing by ProductsController#index as HTML
Rendering products/index.html.erb within layouts/application
Product Load (0.4ms) SELECT "products".* FROM "products"
Rendered products/index.html.erb within layouts/application (21.2ms)
Completed 200 OK in 55ms (Views: 50.8ms | ActiveRecord: 0.4ms)
ได้ผล! Request ที่สองเร็วกว่า request แรกมากกว่า 5 เท่า สังเกตว่า logs ไม่แสดงการ render partials เนื่องจากโหลดจาก cache โดยตรง
การหมดอายุของ Cache
เมื่อเรียก cache helper ในตัวอย่างข้างต้น เราส่ง product object เป็น cache dependency ทำให้ helper รู้ว่า contents ของ cached fragment ขึ้นกับ product object
ภายใน product object มีเมธอด #cache_key ใช้สร้าง key สำหรับ cached fragment Key สำหรับ fragment ทั้งหมดอาจเป็นแบบนี้:
views/products/42-20180302103130041320/75dda06d36880e8b0ae6cac0a44fb56d
Cache key ประกอบด้วยหลายส่วน:
- "views/products" คือ cache class
42คือ product's ID20180302103130041320คือ product's updated_at date75dda06d36880e8b0ae6cac0a44fb56dคือ digest ของ template tree
Key นี้เปลี่ยนเมื่อ product ถูก update หรืออะไรใน template เปลี่ยน เมื่อเปลี่ยน จะเกิด miss ใน cache ทำให้ fragment render ใหม่และเก็บ fragment ใหม่ ทำให้ fragment คง fresh แม้ components จะเปลี่ยน
เกินกว่า Fragment Caching
แม้ cached fragments ในตัวอย่างนี้ให้ speed up เล็กน้อย แต่ caching ยังมีมากกว่าที่เราพูดถึงวันนี้ ด้วยกลยุทธ์อย่าง Russian doll caching หรือ lower level methods อย่าง caching database query results สามารถทำ bigger speedups ได้
เราจะกล่าวถึงใน episode ต่อไปของ AppSignal Academy มีอะไรเฉพาะเจาะจงอยากเรียนรู้? อย่าลังเลติดต่อเราที่ @AppSignal เรายินดีรู้ความคิดเห็นว่าชอบบทความนี้หรือไม่ หรือมีหัวข้ออื่นอยากรู้เพิ่มเติม