Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เพิ่มประสิทธิภาพ Rails ด้วย Fragment Caching

เมื่อ 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 ID
  • 20180302103130041320 คือ product's updated_at date
  • 75dda06d36880e8b0ae6cac0a44fb56d คือ 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 เรายินดีรู้ความคิดเห็นว่าชอบบทความนี้หรือไม่ หรือมีหัวข้ออื่นอยากรู้เพิ่มเติม