Một vài thủ thuật tăng tốc độ ứng dụng Rails

Performance là một ưu tiên lớn cho bất kỳ ứng dụng nào. Tuy nhiên, trong giai đoạn development thì developer thường không quan tâm lắm về hiệu suất cho đến khi nó bắt đầu nhúng vào môi trường production - nơi có số lượng user tăng chóng mặt với lượng dữ liệu lớn. Hiệu suất cần phải là một cái gì đó mà chúng ta phải tập trung ưu tiên giải quyết trước tiên. Ở bài viết này, mình sẽ giới thiệu một số cách để tối ưu performance trong quá trình development:

Database performance

Trong ứng dụng Rails, ORM giúp dễ dàng lấy dữ liệu từ ứng dụng. Tuy nhiên, nó là nguyên nhân làm chúng ta dễ dàng bỏ qua tầm quan trọng và tối ưu việc thao tác với Database.

  • N+1 query:
    Một cách để tránh vấn đề N +1 là thông qua eager loading.
# app/views/customers/index.html.erb
<% @customers.each do |customer| %>
    <%= content_tag :h1, customer.name %>
    <%= content_tag :h2, customer.addresses.first.city %>
<% end %>

Nếu có 100 customers thì sẽ có 101 câu queries. Nếu sử dụng eager loading bằng cách thêm includes:

# app/controller.customers_controller.rb
class CustomersController < ApplicationController
    def index
        @customers = Customer.includes(:addresses).all
    end
end

Bây giờ số query chỉ còn là 2 thay vì 101 như trên. Hãy tưởng tượng nếu số user khoảng vài chục nghìn thì performance sẽ được tối ưu biết bao nhiêu.

  • Add index: Khi đã biết chính xác vấn đề ở đâu, hãy cân nhắc về việc thêm chỉ mục cho Bảng dữ liệu trong Database. Việc tìm kiếm 1 row trên bảng có 1000 rows với chỉ mục sẽ nhanh hơn khoảng 100 lần so với bảng dữ liệu không có chỉ mục.
  class AddIndexForStuff
    def change
      add_index :stuff, :stuff_id
    end
  end

Caching

Caching lưu trữ mọi thứ trong bộ nhớ để sử dụng lặp lại hoặc trong tương lai. Rails làm cho bộ nhớ đệm dễ dàng, mặc dù tốt nhất là loại được sử dụng mà không liên quan đến ứng dụng. Có thể sử dụng những thứ như Nginx để lưu trữ các tệp tin tĩnh. Page-caching của các tập tin tĩnh khi sử dụng Rails và Nginx dễ dàng như sau:

# creates on #index, #show, etc
caches_page :index
        
# expires on #creates, #update, #destroy, etc
expire_page :action => :index

Để page phục vụ đúng các đối tượng lưu trữ trong bộ nhớ cache, cần phải thiết lập Nginx để thực hiện việc này. Sử dụng máy chủ đầu cuối, bạn có thể làm như sau (ví dụ này giả định bạn đang sử dụng Unicorn làm máy chủ web):

upstream upstream_enki {
  server unix:/var/run/engineyard/unicorn_enki.sock fail_timeout=0;
}

location ~ ^/(images|assets|javascripts|stylesheets)/ {
  try_files $uri $uri/index.html /last_assets/$uri /last_assets/$uri.html @app_enki;
  expires 10y;
}

location / {
  if (-f $document_root/system/maintenance.html) {return 503; }
    try_files $uri $uri/index.html @app_enki;
}

Action-caching là một cách khác để cải thiện hiệu suất ứng dụng. Nó giống như page-caching, ngoại trừ toàn bộ nội dung của hành động sẽ được lưu trữ trong kho lưu trữ bộ nhớ cache. Lợi ích là bất kỳ before_filters nào sẽ vẫn được gọi. Thông thường đây là để đảm bảo bất kỳ chức năng xác nhận hoặc đăng nhập sẽ được gọi, trong khi các mục lưu trữ khác có thể được lưu trữ để cải thiện hiệu suất:

before_filter :make_sure_things_are_ok
caches_action :all_the_things

def all_the_things
    @all_things = Thing.all_in_some_way
end

def expire
    expire_action :action => :all_the_things
end

Refactor views code

  • Partials: Partials được xây dựng trong Rails để có thể tái sử dụng những đoạn code view hoặc chia nhỏ từng phần của file view tổng thể, khiến cho chúng ta dễ theo dõi được cấu trúc của 1 file view. Một partial thông thường sẽ được gọi thế này
# app/views/customers/new.html.erb
<h1>New Customer</h1>  
<%= render 'form' %>  
<%= link_to 'Back', customers_path %> 

Nó sẽ render ra file_form.html.erb trong cùng thư mục với file new.html.erb. Trong trang index customers, bạn có thể render ra 1 list các @customers bằng cách:

<%= render @customers %>  

Rails sẽ tìm kiếm partial 'custormer' và sử dụng nó để render mỗi employee trong list @customers collection. Ngoài ra nhiều lúc, bạn cũng sẽ cần đến render partial: 'shared/customer', collection: @customers để render ra một file partial nằm hoàn toàn ở 1 thư mục khác.

  • Decorators Decorator pattern được sử dụng để đóng gói các logic hiển thị mà không thuộc về model, nhưng cũng không thuộc về views. Ví dụ như bạn có một blogging application, hiển thị ngày public của một article theo một format (vd Feb 28th, 2017) hoặc là "Draft" nếu như nó chưa được publish. Khi đó, đoạn code của bạn sẽ là:
<article>  
  <span class="publication-status">
    <% if @article.published? %>
      Published at: <%= @article.published_at.strfitme("%B #{@article.published_at.day.ordinalize}, %Y")
    <% else %>
      Draft
    <% end %>
  </span>
</article>  

Nếu bạn để ý, khi có những logic hiển thị nằm trong view file, chúng ta sẽ rất khó để nhìn ra cấu trúc tổng thể của toàn bộ file view. Bạn có thể nghĩ đến việc dùng View Helpers của Rails như một giải pháp thay thế việc đặt logic ở trong view. Tuy nhiên, sử dụng Helper có một vài hạn chế:

Helpers được include trong tất cả các views, tức là bạn sẽ phải cẩn trọng trong việc đặt tên hàm vì nó sẽ rất dễ bị conflict. Thêm vào đó, 1 view có thể gọi những method mà không có tác dụng gì cho nó thì cũng không hợp lý.

Helpers là các modules, do đó bạn sẽ không thể truy cập từ 1 object. Điều đó có nghĩa là bạn sẽ phải truyền 1 object vào phương thức giống như thế này: article_published_at_date(article)

Draper là một gem rất phổ biến cung cấp cho banj định nghĩa các decorator pattern để mở rộng một Active Record object với logic hiển thị mà không cần đặt hết chúng vào trong model.

class ArticleDecorator < Draper::Decorator  
  delegates_all

  def publication_status
    if is_published?
      "Published at: #{published_at}"
    else
      "Draft"
    end
  end
  
  def published_at
    object.published_at.strfitme("%B #{published_day}, %Y")
  end

  private

  def published_day
    object.published_at.day.ordinalize
  end
end  

Để object của chúng ta gọi được decorator, ta chỉ việc thêm nó vào trong controller như sau:

class ArticlesController < ApplicationController  
  def show
    @article.find(params[:id]).decorate
  end
end  

Khi đó, trong view file sẽ chỉ còn lại là:

<article>  
  <span class="publication-status">
    <%= @article.publication_status %>
  </span>
</article> 
  • Alternate Templating Languages Erb là một template language tốt, đủ dùng và rất phổ biến. Tuy nhiên, một vài ngôn ngữ nâng cao khác như Haml và Slim sẽ giúp cho bạn tối giản hết mức những đoạn code HTML của mình, khiến cho những dòng code trở nên ngắn ngọn hơn rất nhiều.

ERB

<section class=”container”>  
  <h1><%= post.title %></h1>
  <h2><%= post.subtitle %></h2>
  <div class=”content”>
    <%= post.content %>
  </div>
</section>  

Haml

%section.container
  %h1= post.title
  %h2= post.subtitle
  .content
    = post.content

Slim

section.container  
  h1= post.title
  h2= post.subtitle
  .content= post.content

Chọn một template language thích hợp là một việc mang tính cá nhân. Nếu bạn cảm tháy thoải mái với HTML tags, ERB sẽ là một lựa chọn tốt và hỗ trợ rất nhiều cho bạn. Nếu bạn muốn ngắn gọn, nhìn code sạch, dễ nhìn hơn, hãy thử sử dụng Haml hoặc Slim và trải nghiệm.

Nguồn tham khảo: https://www.engineyard.com/blog/improving-rails-app-performance-with-database-refactoring-and-caching https://allenan.com/refactoring-rails-views/