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

ERB vs HAML vs Slim: Template Engine ไหนให้ประสิทธิภาพสูงสุดสำหรับ Ruby?

ในบทความนี้ เราจะทดสอบและวิเคราะห์ประสิทธิภาพของ 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| %>
      
    <% end %>
  
<%= note %>
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

<%= render partial: 'notes_erb/note', collection: @notes %>
Title Description Tasks

ไม่มีอะไรพิเศษ 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 จะเป็นแบบนี้:

    ERB vs HAML vs Slim: Template Engine ไหนให้ประสิทธิภาพสูงสุดสำหรับ Ruby?

    แต่ละ 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:

    ERB vs HAML vs Slim: Template Engine ไหนให้ประสิทธิภาพสูงสุดสำหรับ Ruby?

    แสดง 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!