ในบทความนี้ เราจะทดสอบและวิเคราะห์ประสิทธิภาพของ Ruby Templating Engines 3 ตัวที่นิยมที่สุด: ERB (Default), HAML และ SLIM
Benchmarking คือการเปรียบเทียบกระบวนการทางธุรกิจและเมตริกประสิทธิภาพกับ Best Practices ในอุตสาหกรรม ในขณะที่ Load Testing คือการกระทบความต้องการให้ระบบและวัด Response
เป้าหมายคือสำรวจ Ruby Benchmark Module ซึ่งมีเมธอดวัดและรายงานเวลาที่ใช้ Execute Ruby Code เราจะสร้าง Inline Templates รันเทส และดึง Metrics ทั้ง 3 Engines
หลังจากนั้น เราจะลงลึก Load Testing โดยสร้าง Real-world Template Views ใส่ Live บน Server และ Load Test ผ่าน Tool hey ซึ่งให้ Features ส่ง Load ไป Web App และได้ Data อย่างละเอียดว่าแต่ละ Endpoint (และแต่ละ Template Engine) Perform ยังไง
Setup
ขั้นแรก ต้องแน่ใจว่าติดตั้ง Ruby แล้ว เราใช้เวอร์ชันล่าสุด 2.7.0 ตอนเขียนบทความ ติดตั้ง Rails Gem ด้วย
Ruby และ Rails Gem คือสิ่งที่เราต้องการ สำหรับ Tutorial นี้ ใช้ VS Code เป็น IDE แต่เลือกอันอื่นก็ได้
เลือก Folder สำหรับ Rails Project และรัน:
rails new haml-slim-erb
จะดาวน์โหลด Dependencies และสร้าง Scaffolded Rails Project ไป Explore ได้เลย
ก่อนเขียนโค้ด ต้องเพิ่ม SLIM และ HAML Dependencies ใน Gemfile:
group :development, :test do
gem 'byebug', platform: :mri
gem 'haml'
gem 'slim'
end
ยังมี Bug เริ่มต้นของโปรเจกต์เกี่ยวกับ SQLite Version หาใน Gemfile แล้วเปลี่ยนเป็น:
gem 'sqlite3', '~> 1.3.0'
รัน bundle install เพื่อดาวน์โหลด Dependencies
สำรวจ Benchmark Module
สำหรับส่วนนี้ เราจะทำงานใน Folder test เปิดแล้วจะเห็น Folder ว่าง สร้าง Folder ใหม่ชื่อ benchmark และไฟล์ 3 ไฟล์: example_1_test.rb, example_2_test.rb, example_3_test.rb
Ruby ต้องการไฟล์ท้ายด้วย test ถึงจะถือว่า Test File
เพิ่มเนื้อหาในไฟล์แรก:
require 'benchmark'
number = (0..50).to_a.sort{ rand() - 0.5 }[0..10000]
puts Benchmark.measure {
20_000.times do
number[rand()] * (0..50).to_a.sort{ rand() - 0.5 }[0..10000][rand()]
end
}
บรรทัดแรก Import Benchmark Module แล้วสร้าง Random Array ตัวเลข 0-50 ขนาด 10,000 ตัวเลขใหญ่ๆ เพื่อให้ Processing ใช้เวลา
เมธอด measure มีประโยชน์มาก วางได้ทุกที่ใน Ruby Code วัดเวลา Processing เวลาที่ได้จะ Return และ Print ด้วย puts
ภายใน Loop 20k รอบ Execute การคูณค่า Random จาก Array เดียวกัน
รัน Test File นี้ด้วย:
rake test TEST=test/benchmark/example_1_test.rb
ผลอาจเป็นแบบนี้:
0.702647 0.012353 0.715000 ( 0.721910)
Report นี้ Print ตามลำดับ: User CPU Time, System CPU Time, ผลรวม User+System CPU Times, และ Elapsed Real-time หน่วยเป็นวินาที
เราจะใช้ Benchmark Methods อื่นๆ ต่อไป
ทดสอบ Inline Templates
ตอนเข้าใจ Benchmark Module แล้ว เราจะทดสอบ Templates ทั้ง 3
สร้าง Template เดียว แปล Syntax ทั้ง 3 Engines แล้วรัน Benchmark
เพิ่มใน Test File ที่สอง:
require 'erb'
require 'haml'
require 'slim'
require 'benchmark'
require 'ostruct'
notes = OpenStruct.new(
title: 'Write an essay',
description: 'My essay is about...',
randomList: (0..50).to_a.sort{ rand() - 0.5 }[0..10000]
)
erb_example = <<-ERB_EXAMPLE
<%= notes.title %>
<%= notes.description %>
<% notes.randomList.each do |note| %>
<%= note %>
<% end %>
ERB_EXAMPLE
slim_example = <<-SLIM_EXAMPLE
span= notes.title
span= notes.description
table
tr
- notes.randomList.each do |note|
td= note
SLIM_EXAMPLE
haml_example = <<-HAML_EXAMPLE
%span= notes.title
%span= notes.description
%table
%tr
- notes.randomList.each do |note|
%td= note
HAML_EXAMPLE
context = OpenStruct.new(notes: notes)
__result = ''
Benchmark.bmbm(20) do |bcmk|
bcmk.report("erb_test") {
(1..2000).each {
ERB.new(erb_example, 0, '-', '__result').result binding
}
}
bcmk.report("slim_test") {
(1..2000).each {
__result = Slim::Template.new { slim_example }.render(context)
}
}
bcmk.report("haml_test") {
(1..2000).each {
__result = Haml::Engine.new(haml_example).render(binding)
}
}
end
Import Modules ที่ต้องใช้ นอกจาก Template Engines ยัง Import ostruct OpenStruct คือ Data Structure แบบ Metaprogramming ใกล้เคียง Hash ให้ Define Attributes และ Values ได้โดยไม่ต้องสร้าง Class Structure เต็มๆ
Struct ของเราคือ Object Note มี Title, Description และ List ตัวเลข Random ที่เพิ่ม Processing Time
เราไม่ Focus ว่าแต่ละ Engine ทำงานยังไง (อ่าน Docs ได้) แต่ Syntax เข้าใจง่าย
จุดสำคัญอยู่ท้ายโค้ด ใช้เมธอด bmbm ซึ่งทำมากกว่าเมธอดแรก บางครั้ง Benchmark Results อาจ Skew จาก External Factors เช่น Garbage Collection, Memory Leaks เมธอดนี้พยายามลดผลกระทบโดยรัน 2 รอบ: รอบแรก Rehearsal ทำให้ Runtime Environment Stable รอบที่สองจริง อ่านเพิ่มได้ ที่นี่
แต่ละบรรทัดใน bcmk Loop 2000 รอบ ระหว่างการสร้าง Template แต่ละตัว Focus ที่ Instantiate และ Render
หลังรัน Test ผลลัพธ์:
Rehearsal --------------------------------------------------------
erb_test 0.311534 0.002963 0.314497 ( 0.314655)
slim_test 2.544711 0.004520 2.549231 ( 2.550307)
haml_test 1.449813 0.003169 1.452982 ( 1.454118)
----------------------------------------------- total: 4.316710sec
user system total real
erb_test 0.298730 0.000679 0.299409 ( 0.299631)
slim_test 2.550665 0.004148 2.554813 ( 2.556023)
haml_test 1.432653 0.001984 1.434637 ( 1.435417)
สองบล็อกผลลัพธ์แยกกัน เพื่อให้เห็นว่าอันไหนเป็น Rehearsal อันไหนเป็น Real
เปลี่ยน Scenario เล็กน้อย
จากผลลัพธ์ก่อน อาจคิดว่า ERB ดีที่สุด SLIM แย่ที่สุด แต่ขึ้นอยู่กับ Situation
ในเทสก่อน ทุก Loop ต้อง Instantiate New Template Engine Object ไม่ใช่ Optimal Flow
ลองย้าย Instantiation ออกนอก Loop:
erb_engine = ERB.new(erb_example, 0, '-', '__result')
slim_engine = Slim::Template.new { slim_example }
haml_engine = Haml::Engine.new(haml_example)
Benchmark.bmbm(10) do |bcmk|
bcmk.report("erb_test") { (1..2000).each { erb_engine.result binding } }
bcmk.report("slim_test") { (1..2000).each { __result = slim_engine.render(context) } }
bcmk.report("haml_test") { (1..2000).each { __result = haml_engine.render(binding) } }
end
โค้ดทำเหมือนเดิม รันอีกครั้ง ผลลัพธ์:
Rehearsal ----------------------------------------------
erb_test 0.127599 0.002407 0.130006 ( 0.130137)
slim_test 0.046972 0.000841 0.047813 ( 0.047858)
haml_test 0.208308 0.002239 0.210547 ( 0.210769)
-------------------------------------- total: 0.388366sec
user system total real
erb_test 0.118002 0.000556 0.118558 ( 0.118618)
slim_test 0.040129 0.000090 0.040219 ( 0.040320)
haml_test 0.205331 0.001163 0.206494 ( 0.206680)
สังเกตว่าตอนนี้ตัวไหนดีที่สุด แย่ที่สุด แค่แสดงว่าไม่มี Silver Bullet สำหรับ Engine ที่สมบูรณ์แบบ
คุณต้อง Test และวิเคราะห์ว่า Feature ไหน Perform ดีกว่าสำหรับ Coding Style ของคุณ ยังมี Tools อื่นๆ เช่น Ruby Profiler ช่วยเข้าใจว่าโค้ดทำงานยังไง ทำไมถึงเป็นแบบนี้
Load Testing สถานการณ์ Real-world
ไปสู่สิ่งที่ใกล้ความเป็นจริง เราจะ Benchmark Real Template ที่ List Notes (สามตัว หนึ่งต่อ Engine)
เพราะ benchmark Module รันใน Rails Code เราสูญเสีย Measures สำคัญบางอย่างเกี่ยวกับ Internal Processes ของ Template Engine
ประเภท Benchmark นี้ช่วยให้เห็นว่าแต่ละ Engine Perform ยังไงโดยรวม ตั้งแต่ Request เข้ามา Business Logic Processing จน Response ถึง Views ขั้นสุดท้ายนี้มี Steps มาก เช่น Parsing และ Rendering ที่ Load Test วัดได้ แต่ Benchmark ไม่ได้
ก่อนอื่น สร้าง Rails Controllers สำหรับแต่ละตัวอย่าง:
rails g controller notes_erb index
rails g controller notes_haml index
rails g controller notes_slim index
คำสั่งนี้ Auto-generate Rails Files ทั่วไป
เปิด notes_erb_controller.rb แล้วเปลี่ยนเนื้อหา:
class NotesErbController < ApplicationController
def index
@notes = JSON.parse(Constants::NOTES, object_class: OpenStruct)
end
end
ที่นี่เรา Feed Data ให้ Templates มี Controller Class หนึ่งต่อ Engine
เรา Grab JSON จาก Inline Constant Parse ให้เป็น OpenStruct Object ส่งให้ Engines
Constant NOTES ต้องวางในไฟล์ใหม่ constants.rb สร้างแล้วเพิ่ม:
class Constants
NOTES = '[ { "title": "Walk the dog", "description": "Bla bla", "tasks": [{ "title": "Task #1" }, { "title": "Task #2" }, { "title": "Task #3" }] }, ... { "title": "Walk the dog", "description": "Bla bla", "tasks": [{ "title": "Task #1" }, { "title": "Task #2" }, { "title": "Task #3" }] } ] '
end
เปลี่ยน Ellipsis ให้มี Note Elements มากขึ้น และ Replicate Logic ใน Controllers อื่นๆ ด้วย
สร้าง ERB Views
ไปที่ Folder views/notes_erb สร้าง note.html.erb และ task.html.erb ช่วยสร้างตัวอย่างที่มี Views, Partials, Layouts
ทำให้ Example Explore Rails Engines มากที่สุด สร้างไฟล์เทียบเท่าสำหรับ views/notes_haml และ views/notes_slim ด้วย
เริ่ม index.html.erb:
List of Notes
Title
Description
Tasks
<%= render partial: 'notes_erb/note', collection: @notes %>
ไม่มีอะไรพิเศษ Import Partial _note.html.erb ส่ง Parameter collection: @notes ที่สร้างใน Controller
เนื้อหา _note.html.erb:
<%= note.title %>
<%= note.description %>
<%= render partial: 'notes_erb/task', collection: note.tasks %>
อีก Partial เข้าถึง Array tasks
เนื้อหา _task.html.erb:
<%= task.title %>
HAML Views
Syntax จาก Engine หนึ่งไปอีกหนึ่ง คล้ายมาก แค่ Verbosity ต่างกัน SLIM สะอาดที่สุด
ดูโค้ด 3 ไฟล์:
# Content of index.html.haml
:css
h2 { text-align: center; }
table, td, th { border: 1px solid #ddd; text-align: left; }
table { border-collapse: collapse; width: 80%; margin: auto; }
th, td { padding: 15px; }
%h2 List of Notes
%table
%thead
%tr
%th Title
%th Description
%th Tasks
%tbody
= render partial: 'notes_haml/note', collection: @notes
# Content of _note.html.haml
%tr
%td
%span= note.title
%td
%span= note.description
%td
%ul
= render partial: 'notes_haml/task', collection: note.tasks
# Content of _task.html.haml
%li= task.title
คล้ายกันมากใช่ไหม?
SLIM Views
สุดท้าย SLIM Views ความต่างมากที่สุด โครงสร้างชัดเจนขึ้น:
# index.html.slim
css:
h2 { text-align: center; }
table, td, th { border: 1px solid #ddd; text-align: left; }
table { border-collapse: collapse; width: 80%; margin: auto; }
th, td { padding: 15px; }
h2 List of Notes
table
thead
tr
th Title
th Description
th Tasks
tbody
= render partial: 'notes_haml/note', collection: @notes
# _note.html.slim
tr
td
span= note.title
td
span= note.description
td
ul
= render partial: 'notes_haml/task', collection: note.tasks
# _task.html.slim
li= task.title
ต้อง Translate Layouts ให้แต่ละ Engine Syntax สร้าง 2 ไฟล์ใหม่ใน views/layouts: application.html.haml และ application.html.slim
ทิ้งเป็น Homework ให้ แต่ถ้ายาก ดู Version ของผมใน GitHub Project Link ท้ายบทความ
รัน Tests
สุดท้ายถึงทดสอบ รัน rails s เริ่มที่ https://localhost:3000/
View จะเป็นแบบนี้:

แต่ละ Engine Example ใช้ URL ที่ Auto-generate ใน config/routes.rb
Benchmark ด้วย hey Tool เรียบง่าย ให้ Info มีประโยชน์ คำสั่ง:
$ hey https://localhost:3000/notes_erb/index
Summary:
Total: 9.3978 secs
Slowest: 9.1718 secs
Fastest: 0.0361 secs
Average: 1.2714 secs
Requests/sec: 21.2816
$ hey https://localhost:3000/notes_haml/index
Summary:
Total: 10.8661 secs
Slowest: 10.2354 secs
Fastest: 0.1871 secs
Average: 1.4735 secs
Requests/sec: 18.4058
$ hey https://localhost:3000/notes_slim/index
Summary:
Total: 11.3384 secs
Slowest: 10.7570 secs
Fastest: 0.0437 secs
Average: 1.5406 secs
Requests/sec: 17.6392
เห็นว่า Engines ทั้ง 3 ใกล้เคียงกันมาก Default Requests คือ 200 แตะเปลี่ยนได้ด้วย Option -n
ลอง 1200 Requests:
$ hey -n 1200 https://localhost:3000/notes_erb/index
Summary:
Total: 52.2586 secs
Slowest: 19.2837 secs
Fastest: 0.0389 secs
Average: 0.6960 secs
Requests/sec: 22.9627
$ hey -n 1200 https://localhost:3000/notes_haml/index
Summary:
Total: 61.7637 secs
Slowest: 18.5290 secs
Fastest: 0.0442 secs
Average: 0.8557 secs
Requests/sec: 19.4289
$ hey -n 1200 https://localhost:3000/notes_slim/index
Summary:
Total: 63.1625 secs
Slowest: 19.9744 secs
Fastest: 0.0874 secs
Average: 0.7959 secs
Requests/sec: 18.9986
เพิ่ม Concurrent Requests จะเห็นความต่าง Total และ Average Processing Times เพิ่มขึ้น สถานการณ์นี้กับพันๆ Parallel Requests เฉพาะเจาะจง ไม่ค่อยพบ แต่ Load Testing คือการทดสอบขีดจำกัด
hey ยัง Print Info อื่นๆ เช่น Response Time Histogram:

แสดง Average Time แต่ละ Request ใช้ Complete แบบประมาณ ในตัวอย่าง Requests ส่วนใหญ่ (1048) Complete ใน 1.893 วินาที เพราะฉะนั้น Stress Tests ควรทำพร้อมกัน
ยังมี Latency Distribution, DNS Dialup/Lookup, Request Writing/Waiting/Reading Times, Errors ฯลฯ ดู Docs สำหรับ Options/Results เพิ่มเติม
สรุป
Source Code ตัวอย่าง ที่นี่
Test แบวนี้ช่วยตัดสินใจ Template Engine ไหนเหมาะกับโปรเจกต์ ควรระวัง Poorly Implemented Code เพราะ Code Snippets เล็กๆ อาจเปลี่ยน Overall Performance درามัติค
จุดน่าสนใจ: ERB เป็น Default Rails Engine ทำงานดีมาก แม้ Engines อื่นเร็วกว่าในบาง Test ERB ใกล้เคียงพอที่จะพิสูจน์คุณค่า
สุดท้าย แนะนำพิจารณา Factors อื่นๆ ใน Tests เป็น Homework: Caching, Proxy, Database Usage, Storing Mechanisms, Queues, Async Tools ทั้งหมดเล่น角色 สำคัญในพฤติกรรม Views และ Rendering Time Good luck!