Rails fragment caching tạo greater speedups khi bigger part của page được cache. Điều này difficult hơn cho pages với nhiều dynamic hoặc user-specific content. Solution là dùng "JavaScript sprinkles", giống hagelslag (v lạc choco), nhưng không chocolate và với extra requests load user-specific content sau khi rest of page served trực tiếp từ cache.
Fragment caching
Kỹ thuật như fragment caching dùng speed up responses Rails apps bằng cách cache fragments của rendered page. Dùng smart cache keys, fragments tự invalidate khi content thay vì vì data shown in view updated.
Caching view fragments cung cấp larger speed boost từ smaller cache khi cached fragments dùng more often. Khi content page depend trên currently signed in user, caching page fragments trở nên difficult hơn.
Unread Responses (Responses chưa đọc)
Ví dụ, Rails-based blog. Mỗi article có thể có số responses. Authentication system added cho user sign in và post responses với account. Trên mỗi article page, list responses. Để dễ cho user, mark new responses với icon và background color khác.
Nếu wrap mỗi article trong cache block, risk mark wrong responses as unread. Khi user request index page, response cached. Khi user khác request same page sau, họ nhận cached fragment, với unread responses của user đầu.
# app/views/articles/show.html.erb
<%= cache(@article) do %>
<h1><%= @article.title %></h1>
<%= simple_format(@article.content) %>
<section id="responses">
<h2>Responses</h2>
<% @article.responses.each do |response| %>
<div class="<%= response.read_by?(@current_user) ? 'read' : 'unread' %>">
<%= time_tag(response.created_at) %>
<strong><%= response.name %></strong>:
<%= response.content %>
</div>
<% end %>
</section>
<% end %>
Cách solve: add currently signed in user vào cache key dùng [@article, @current_user] thay vì chỉ @article argument pass cho cache helper.
# app/views/articles/show.html.erb
<%= cache([@article, @article]) do %>
<h1><%= @article.title %></h1>
# ...
<% end %>
Điều này ensure mỗi user thấy unread responses của họ, nhưng negate most speedup từ caching fragments, vì article fragments cached separately cho mỗi user.
JavaScript Sprinkles
Vì hầu hết page same cho all users, ta muốn reuse cached article fragment cho mỗi visitor. Để làm này, load article từ cache và add extra request fetch user-specific content sau page loaded qua JavaScript request. Dùng sprinkle JavaScript functionality cho này có vài advantages:
- Page cached once reused cho unauthenticated guests và other users không separately cache fragments cho mỗi user.
- Most important content load first cho fastest response time và secondary features, như unread count, load later.
- Vì extra request qua JavaScript, whole page có thể edge-cached trên CDN để further improve performance.
Cleanup
First remove dynamic content từ pages để easier cache. Remove @current_user từ cache key trong cache block, không còn cached cho mỗi user. Sau đó, remove query find unread counts từ controller, remove CSS class names từ view.
# app/views/articles/show.html.erb
<%= cache(@article) do %>
<h1><%= @article.title %></h1>
<%= simple_format(@article.content) %>
<section id="responses">
<h2>Responses</h2>
<% @article.responses.each do |response| %>
<div data-response-id="<%= response.id %>">
<%= time_tag(response.updated_at) %>
<strong><%= response.name %></strong>:
<%= response.content %>
</div>
<% end %>
</section>
<% end %>
Left với generic page easier cache nhưng thiếu unread responses feature. Hãy add back.
The Endpoint
First tạo endpoint find unread responses cho user. Muốn alter current state page sau rendered, request JSON từ nó.
# app/controllers/unread_responses_controller.rb
class UnreadResponsesController < ApplicationController
def index
@article = Article.find(params[:article_id])
@responses = @article.unread_responses_for(@current_user)
end
end
# app/views/unread_responses/index.json.jbuilder
json.array! @responses do |response|
json.extract! response, :id
end
# config/routes.rb
Rails.application.routes.draw do
resources :articles do
resources :responses
resources :unread_responses
end
end
Endpoint produce list unread response IDs.
# GET /articles/1/unread_responses.json
[{"id":1},{"id":2},{"id":3}]
Tip: Khi load dynamic component có thể pre-render server side, usually faster render HTML server side, then inject HTML vào page trực tiếp via JavaScript.
Showing Unread Responses
Thay vì hardcode URL unread responses endpoint trong JavaScript, add vào data attribute trong view để refer later.
# app/views/articles/show.html.erb
<section id="responses" data-url="<%= article_unread_responses_path(@article, json: true) %>">
# ...
</section>
Sau page loaded hoàn toàn, request unread response IDs từ endpoint mới. Dùng data từ list mark each article as unread bằng add CSS class.
// app/assets/javascripts/application.js
document.addEventListener("turbolinks:load", function () {
responses = document.getElementById("responses");
if (!responses.dataset.loaded) {
Rails.ajax({
url: responses.dataset.url,
type: "GET",
success: function (data) {
responses.dataset.loaded = true;
data.forEach(function (response) {
element = document.querySelector(
"[data-response-id='" + response.id + "']"
);
element.classList.add("unread");
});
},
});
}
});
Vì Rails app dùng Turbolinks, wait page loaded bằng listen turbolinks:load event. Khi event fire, find responses box dùng ID.
Check nếu responses element có loaded data attribute. Set attribute sau update unread responses sau, để không make extra requests nếu page reloaded dùng browser back button.
First load, loaded attribute chưa set, continue make request endpoint. On success, loop over each article in returned result, find response element by ID, add "unread" CSS class.
Sprinkles!
Fragment caching reusable content và sprinkling JavaScript onto pages add user-specific dynamic bits sau có thể làm app initial response time faster bằng serve most important content straight từ cache. Mặc dù tốn time extra request và update page sau main content loaded, deferring dynamic content làm app feel snappier, ngay cả khi full request lâu hơn serve everything包括 user-specific parts từ cache.
Cho elaborate setups, check out Stimulus, JavaScript library wrap sprinkles pattern vào framework tie HTML views đến JavaScript.
Hy vọng bạn thích intro JavaScript sprinkles trong Rails apps. Comment về bài này, blog hoặc topic khác muốn cover, drop line @AppSignal.