How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

How-to Migration

Move from server-rendered to hybrid without a rewrite

A site whose public pages are rendered for every visitor because of one personal element can be moved to cached pages with fragments one element at a time, keeping every template it has.

Context

Context. The site renders every public page for every visitor, because the header greets members by name, shows a cart count, or reads a cookie, and someone added preventCaching() to make it correct. It works, it is slow under load, and it gets slower as traffic grows. Moving to the hybrid pattern solves it without a rewrite into a JavaScript application: cache the page, and fetch the few personal parts after it has loaded (Hybrid: cached pages with dynamic parts). It can be done one element at a time, with the site working after every step.

Steps

  1. Measure the starting point: request each main page type twice signed out and note X-Cache-Status and the time to first byte (cacheability).
  2. Find the personal output. Search the templates used on public pages for preventCaching, $_currentUser, $_cart and getCookie. Each hit is either a reason a page is not cached or an element that would be wrong if it were.
  3. Pick one element, the one on the most pages, usually the account links in the header.
  4. Move it into a fragment: a small template that renders only that element and prevents caching, served by a page that uses a fragment layout.
  5. Replace it in the page with a placeholder holding the signed-out version, and a script that swaps in the fragment.
  6. Remove that element's reason for preventCaching() from the public template. When the last one is gone, remove the call.
  7. Release, clear the cache, measure again, and repeat with the next element.

Code

Before, in the header of every public page:

Template code
{$_response->preventCaching()}
{if $_currentUser->isLoggedIn()}
  <a href="/my-account">My account</a> <a href="/logout">Sign out</a>
{else}
  <a href="/login">Sign in</a>
{/if}
{% do _response.preventCaching() %}
{% if _currentUser.isLoggedIn() %}
  <a href="/my-account">My account</a> <a href="/logout">Sign out</a>
{% else %}
  <a href="/login">Sign in</a>
{% endif %}

After, the same markup moves unchanged into a fragment template, served at /fragments/account by a page with the fragment layout:

Template code
{* templates/fragments/account.html *}
{$_response->preventCaching()}
{if $_currentUser->isLoggedIn()}
  <a href="/my-account">My account</a> <a href="/logout">Sign out</a>
{else}
  <a href="/login">Sign in</a>
{/if}
{# templates/fragments/account.html #}
{% do _response.preventCaching() %}
{% if _currentUser.isLoggedIn() %}
  <a href="/my-account">My account</a> <a href="/logout">Sign out</a>
{% else %}
  <a href="/login">Sign in</a>
{% endif %}

and the header keeps a placeholder, with the signed-out version inside it, and a script that fills it:

Template code
<div id="account-links"><a href="/login">Sign in</a></div>
<script nonce="{$_inlineScriptNonce}">
  fetch('/fragments/account', { credentials: 'same-origin' })
    .then(r => r.ok ? r.text() : null)
    .then(html => { if (html) document.getElementById('account-links').innerHTML = html; });
</script>
<div id="account-links"><a href="/login">Sign in</a></div>
<script nonce="{{ _inlineScriptNonce }}">
  fetch('/fragments/account', { credentials: 'same-origin' })
    .then(r => r.ok ? r.text() : null)
    .then(html => { if (html) document.getElementById('account-links').innerHTML = html; });
</script>

The fragment layout and page are set up once and reused for every later element (dynamic fragments); the cart count and other elements follow the same shape (a live cart count on a cached page).

Why this and not that

Rewriting the front end as a JavaScript application would also separate the cached and the personal, at the cost of every template, the site's search engine visibility and months of work; the hybrid move keeps every template and changes only the elements that are personal. Moving one element per release keeps the site correct at every step: the page is cacheable only once nothing personal is left in it, and the measurement after each step shows the gain. The placeholder holds the signed-out version so that the page is complete before the script runs and without it, and so that a failed fragment request leaves a working link rather than a hole.

Go deeper

Was this page helpful?

Your answer goes to the docs team.