Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

ERB vs HAML vs Slim: Engine Template Ruby Nào Cho Hiệu Suất Tối Ưu?

Trong bài viết này, chúng ta sẽ kiểm tra và phân tích hiệu suất của ba engine template Ruby phổ biến nhất: ERB (mặc định), HAML, và SLIM.

Benchmarking là thực hành so sánh quy trình kinh doanh và metrics hiệu suất với industry bests và best practices từ các công ty khác. Trong khi đó, load testing là quá trình đặt yêu cầu lên hệ thống và đo phản hồi của nó.

Mục tiêu của chúng ta là khám phá một chút module Benchmark của Ruby, cung cấp các phương thức để đo lường và báo cáo thời gian dùng để thực thi code Ruby. Chúng ta sẽ tạo một số inline template, chạy chúng qua các test, và trích xuất metrics qua ba engine khác nhau.

Sau đó, chúng ta sẽ đi sâu vào load testing bằng cách tạo một số real-world template view, đưa chúng lên server, và thực hiện một số load test qua công cụ hey. Nó cung cấp cho chúng ta một số tính năng tuyệt vời để gửi load đến một web application cụ thể và lấy dữ liệu kỹ lưỡng liên quan đến cách mỗi endpoint và do đó mỗi template engine đang hoạt động.

Thiết Lập

Điều đầu tiên, tất nhiên, là đảm bảo bạn đã cài Ruby. Chúng ta đang dùng phiên bản mới nhất, 2.7.0, tính tại thời điểm viết bài. Đảm bảo cũng cài gem Rails.

Ruby và gem Rails là gần như tất cả những gì chúng ta cần để bắt đầu. Cho tutorial này, Visual Studio Code là IDE cho coding, nhưng fự tự do chọn cái khác nếu bạn thích.

Bây giờ, chọn một folder cho project Rails của chúng ta và chạy lệnh sau:

rails new haml-slim-erb

Điều này sẽ tải tất cả dependencies cần thiết và tạo project Rails scaffolded. Tự do khám phá nó.

Trước khi tiến hành code, chúng ta cần thêm dependencies SLIM và HAML vào Gemfile:

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platform: :mri
  gem 'haml'
  gem 'slim'
end

Cũng có một bug đi kèm mặc định với project liên quan đến phiên bản SQLite. Tìm nó trong Gemfile và thay đổi thành:

gem 'sqlite3', '~> 1.3.0'

Bây giờ, chạy lệnh bundle install để tải dependencies.

Khám Phá Module Benchmark

Cho phần này của bài viết, chúng ta sẽ làm việc trực tiếp với folder test. Mở nó, bạn sẽ thấy một số folder trống. Hãy tạo một cái mới tên benchmark và ba file khác: example_1_test.rb, example_2_test.rb, và example_3_test.rb..

Ruby cần file kết thúc bằng test để được coi là test file.

Tiếp theo, thêm nội dung sau vào file đầu tiên:

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
}

Lưu ý dòng đầu tiên import module Benchmark bắt buộc. Sau đó, chúng ta generate một mảng ngẫu nhiên số từ 0 đến 50 với size 10.000. Những con số lớn này chỉ để tốn thời gian xử lý.

Phương thức measure rất hữu ích vì nó có thể đặt ở đâu đó trong code Ruby để đo mất bao nhiêu thời gian để xử lý. Thời gian này được trả về và in ra bởi puts.

Bên trong, chúng ta lặp 20k lần thực thi mảng ngẫu nhiên giống nhau để nhân một giá trị của mỗi cái.

Để chạy file test này cụ thể, phát hành lệnh sau:

rake test TEST=test/benchmark/example_1_test.rb

Kết quả có thể trông như thế này:

0.702647 0.012353 0.715000 ( 0.721910)

Báo cáo này in ra, lần lượt, user CPU time, system CPU time, tổng user và system CPU time, và elapsed real-time. Đơn vị thời gian là giây.

Chúng ta sẽ sử dụng các phương thức Benchmark khác trong thực hành.

Testing Inline Templates

Bây giờ bạn hiểu thêm về cách module Benchmark của Ruby hoạt động, chúng ta sẽ đi sâu vào thực thi một số test trên ba template.

Cho việc này, chúng ta sẽ tạo một template đơn, dịch nó thành cú pháp ba engine, và cuối cùng, chạy nó dưới phương thức Benchmark.

Thêm sau vào file test thứ hai:

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
<span><%= notes.title %></span>
<span><%= notes.description %></span>
<table>
  <tr>
    <% notes.randomList.each do |note| %>
    <td><%= note %></td>
    <% end %>
  </tr>
</table>
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

Đầu tiên, import các module bắt buộc. Ngoài các template engine, chúng ta cũng import module ostruct. Một OpenStruct là cấu trúc dữ liệu từ metaprogramming, tương tự Hash, cho phép định nghĩa thuộc tính tùy ý với giá trị đi kèm.

Nó hữu ích vì chúng ta không cần tạo toàn bộ cấu trúc class để lưu giá trị. Chúng ta có thể định nghĩa inline.

Struct của chúng ta cơ bản là một object Note với title, description, và một list số ngẫu nhiên, làm tăng thời gian xử lý.

Chúng ta sẽ không tập trung vào cách mỗi template engine hoạt động; bạn có thể tham khảo docs chính thức của chúng. Tuy nhiên, cú pháp của chúng khá dễ nắm bắt.

Phép màu đặt ở cuối code. Bây giờ, chúng ta dùng phương thức bmbm, làm thêm chút hơn phương thức đầu tiên. Đôi khi kết quả benchmark có thể bị lệch do yếu tố bên ngoài, như garbage collection, memory leaks, v.v. Phương thức này cố gắng minimize hiệu ứng này bằng cách chạy test hai lần: lần đầu tiên như một buổi tập để ổn định môi trường runtime và lần thứ hai cho thật. Bạn có thể đọc thêm ở đây.

Cuối cùng, mỗi dòng code bên trong bcmk thực thi một vòng lặp 2000 lần trong quá trình tạo từng template. Focus là trên việc gán template và render nó.

Sau khi chạy file test này, đây là kết quả:

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)

Hai khối kết quả được tách biệt để bạn hiểu được gì là thật từ buổi tập.

Thay Đổi Tình Huống Một Chút

Đối với kết quả test cuối, bạn có thể giả định ERB là lựa chọn tốt nhất, trong khi SLIM là tệ nhất. Lại, nó phụ thuộc vào tình huống.

Trong test đó, mỗi lần lặp, chúng ta phải instantiate một object template engine mới. Đây không phải là luồng tối ưu.

Hãy thay đổi nó một chút và di chuyển việc instantiate ra ngoài, như trong code snippet sau:

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

Code làm chính xác giống như trước. Bây giờ, chạy test lại, và bạn sẽ thấy gì đó như thế này:

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)

Chú ý cái nào là tốt nhất và tệ nhất bây giờ. Đây chỉ để cho thấy không có silver bullet về engine hoàn hảo.

Bạn phải test và phân tích feature nào perform tốt hơn cho style coding của bạn. Ngoài ra, có các công cụ phụ trợ khác, như Ruby Profiler, bạn có thể dùng để hiểu tốt hơn cách và tại sao code behave cách này.

Load Testing Một Tình Huống Thực Tế

Hãy chuyển sang cái gần thực tế hơn. Chúng ta sẽ benchmark một template thực sự liệt kê một số notes (ba, một cho mỗi template engine).

Vì module benchmark diễn ra trong code Rails, chúng ta mất một số measure quan trọng liên quan đến quy trình nội bộ của template engine.

Loại benchmark test này cho phép chúng ta thấy mỗi engine perform như thế nào như một toàn thể, từ đầu quy trình qua việc đến của request, đến xử lý business logic cho đến khi response data đến views. Cái cuối cùng, cụ thể, sẽ có một loạt bước, như quá trình parsing và rendering mà load test có thể measure, và benchmark không thể.

Đầu tiên, hãy tạo Rails controllers cho mỗi ví dụ:

rails g controller notes_erb index
rails g controller notes_haml index
rails g controller notes_slim index

Lệnh này sẽ auto-generate một loạt file Rails chung bạn có thể đã quen.

Tiếp theo, hãy mở file notes_erb_controller.rb vừa tạo và thay đổi nội dung thành:

class NotesErbController < ApplicationController
  def index
    @notes = JSON.parse(Constants::NOTES, object_class: OpenStruct)
  end
end

Đây là nơi chúng ta feed template với data. Lưu ý chúng ta có một controller class cho mỗi engine.

Cơ bản, chúng ta lấy một số JSON từ một constant inline. Response sẽ được parse thành một object OpenStruct mới và trả về cho các engine.

Constant NOTES phải được đặt vào file mới tên constants.rb. Tạo nó, và thêm nội dung sau:

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

Hãy chắc chắn thay đổi ellipsis cho thêm note elements. Ngoài ra, đừng quên replicate logic trong mỗi controller khác.

Tạo ERB Views

Bây giờ, đến lúc tạo views cho ví dụ của chúng ta. Để làm vậy, vào folder views/notes_erb và tạo hai file khác: note.html.erb và task.html.erb. Chúng sẽ giúp chúng ta xây dựng ví dụ chứa views, partials, và layouts.

Cách này, chúng ta đảm bảo ví dụ của chúng ta explore Rails engines nhiều nhất. Hãy chắc chắn tạo file tương đương cho cả views/notes_haml và views/notes_slim.

Hãy bắt đầu với code index.html.erb:

<style>
  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; }
</style>
<h2>List of Notes</h2>
<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Description</th>
      <th>Tasks</th>
    </tr>
  </thead>
  <tbody>
    <%= render partial: 'notes_erb/note', collection: @notes %>
  </tbody>
</table>

Không có gì quá đặc biệt ở đây. Lưu ý chúng ta đang import một partial, file _note.html.erb, và truyền tham số collection: @notes đã tạo trước đó trong controller.

Đây là nội dung note:

<tr>
  <td>
    <span><%= note.title %></span>
  </td>
  <td>
    <span><%= note.description %></span>
  </td>
  <td>
    <ul>
      <%= render partial: 'notes_erb/task', collection: note.tasks %>
    </ul>
  </td>
</tr>

Đây là partial khác, truy cập mảng tasks lần này.

Nội dung cho _task.html.erb là:

<li><%= task.title %></li>

Views Của HAML

Bạn sẽ nhận thấy cú pháp từ engine này sang engine khác rất giống nhau. Điều thay đổi chỉ là verbosity của mỗi cái. SLIM, ví dụ, là sạch nhất trong số chúng.

Nhìn code cho ba file:

# 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

Rất giống nhau, phải không?

Views Của SLIM

Cuối cùng, chúng ta có views của SLIM. Đây là khác biệt lớn nhất so với hai cái kia. Toàn bộ cấu trúc trở nên rõ ràng hơn:

# 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

Bạn cũng sẽ phải dịch layouts sang cú pháp từng engine. Hai file mới phải được tạo dưới folder views/layouts: application.html.haml và application.html.slim.

Tôi để việc đó cho bạn làm bài tập về nhà. Tuy nhiên, nếu bạn thấy khó khăn, bạn có thể tham khảo phiên bản của tôi trong link project GitHub tại cuối bài viết.

Chạy Test

Cuối cùng, chúng ta đến test ví dụ. Đầu tiên, khởi động application bằng lệnh rails s. Nó sẽ start tại https://localhost:3000/.

Đây là view trông như thế nào:

ERB vs HAML vs Slim: Engine Template Ruby Nào Cho Hiệu Suất Tối Ưu?

Mỗi ví dụ template engine sẽ có sẵn tại URL tương ứng auto-generated trong file config/routes.rb.

Để benchmark test các ví dụ này, chúng ta sẽ dùng công cụ benchmark hey. Nó rất đơn giản và cung cấp một số thông tin hữu ích cho phân tích benchmark. Đ đây là các lệnh:

$ 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

Như bạn thấy, tất cả engine rất gần nhau về execution time. Số request mặc định là 200, nhưng bạn có thể thay đổi giá trị này qua option -n.

Hãy nhìn cùng test được thực hiện với 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

Khi bạn tăng số concurrent requests, bạn sẽ thấy sự khác biệt trong total và average processing times tăng lên. Rõ ràng, kịch bản này, với hàng nghìn parallel requests, rất cụ thể và không quá phổ biến. Tuy nhiên, load testing chính là về việc đó, đưa endpoints đến giới hạn.

Công cụ hey cũng in ra thông tin khác, như response time histogram:

ERB vs HAML vs Slim: Engine Template Ruby Nào Cho Hiệu Suất Tối Ưu?

Nó cho thấy average time mỗi request mất để hoàn thành theo thuật ngữ xấp xỉ. Trong ví dụ của chúng ta, rõ ràng hầu hết requests (1048) hoàn thành trong 1.893 giây. Đó là lý do tại sao đáng để thực hiện stress tests cùng lúc.

Cũng có thêm info về latency distribution, chi tiết qua DNS dialup và lookup, request writing, waiting và reading times, errors, v.v.

Check docs để biết thêm custom options/results.

Tóm Tắt

Bạn có thể tìm source code cho ví dụ này ở đây.

Loại test này rất tốt giúp bạn define template engine nào có thể phù hợp hơn cho nhu cầu project. Như chúng ta đã thấy, hãy cẩn thận về code implement kém vì một số code snippet nhỏ có thể thay đổi đột ngột overall performance của execution.

Điểm thú vị khác là ERB, như engine mặc định của Rails, cũng giỏi việc của nó. Bên cạnh các engine khác nhanh hơn trong một số test của chúng ta, ERB luôn đủ gần để chứng minh giá trị của nó.

Cuối cùng, tôi khuyên bạn cân nhắc một số yếu tố quan trọng khác trong test làm bài tập về nhà, bao gồm caching, proxy, việc sử dụng database, và các cơ chế lưu trữ khác, cũng như queues và bất kỳ tool async nào. Những mục này luôn đóng vai trò quan trọng trong cách views behave hoặc processing time render chúng. Chúc may mắn!