Rendering & Caching

a journey through the layers


Wim Leers
Principal Software Engineer
Acquia logo's Office of the CTO

Principle of caching
inputs → 🤖…💪…🤖…💪…🤖…💪→ output
inputs → cache ID → read → output
Changed inputs: cache tags + contexts
tagsstored data🏓   →   🏂
contextscircumstances☂️  ↔  ☔️
max-agefreshness🕰️         📈
😵
wimleers.com/talk/drupal-8-render-pipeline
/**
 * @Block(id = "journey_talk_uncacheable_block")
 */
class UncacheableBlock extends BlockBase {

  public function build() {
    usleep(250*1000); // Pretend we're computing a better forecast 👍
    return [
      '#markup' => $this->t('

Weather forecast at @time: ☔️.

', [ '@time' => (int) microtime(TRUE), ]), ]; } public function getCacheMaxAge() { return 0; } }
/**
 * @Block(id = "journey_talk_personalized_per_session_block")
 */
class PersonalizedPerSessionBlock extends BlockBase {

  public function build() {
    $funny_emojis = ['🤷', '😂', '🙈', '🐷', '🐑'];
    return [
      '#markup' => $this->t('

Funny emoji just for you: @emoji

(Hand-picked at @time!)

', [ '@emoji' => $funny_emojis[rand(0, count($funny_emojis) - 1)], '@time' => (int) microtime(TRUE), ]), ]; } public function getCacheContexts() { return ['session']; } }
/**
 * @Block(id = "journey_talk_personalized_per_user_block")
 */
class PersonalizedPerUserBlock extends BlockBase {

  public function build() {
    $funny_emojis = ['🤷', '😂', '🙈', '🐷', '🐑'];
    return [
      '#markup' => $this->t('

Today\'s funny emoji just for you: @emoji

(Hand-picked at @time!)

', [ '@emoji' => $funny_emojis[rand(0, count($funny_emojis) - 1)], '@time' => (int) microtime(TRUE), ]), ]; } public function getCacheContexts() { return ['user']; } public function getCacheMaxAge() { return 86400; } }
/**
 * @Block(id = "journey_talk_cacheable_block")
 */
class CacheableBlock extends BlockBase {

  public function build() {
    return [
      '#markup' => $this->t('%name runs this site!', [
        '%name' => User::load(1)->getAccountName(),
      ]),
    ];
  }

  protected function blockAccess(AccountInterface $account) {
    return AccessResult::allowedIf($account->id() != 1);
  }

  public function getCacheTags() {
    return ['user:1'];
  }

}

👩‍💻

👇

download demo module

🎉 Live debug! 🔥

class CacheTagsTest extends BrowserTestBase {

  // Tests that for cached pages, content changes are visible immediately.
  public function testBlackbox() {
    $this->assertThing('initial title');

    Node::load(1)->setTitle('foobar')->save();

    $this->assertThing('foobar');
  }

  protected function assertThing($title) {
    $this->drupalGet('/some/path');
    $this->assertRaw($title);
  }

}
class CacheTagsTest extends BrowserTestBase {

  // Tests that for cached pages, content changes are visible immediately.
  public function testWhitebox() {
    $this->assertThing('initial title', 'MISS', 'MISS');
    $this->assertThing('initial title', 'HIT', 'HIT');

    Node::load(1)->setTitle('foobar')->save();

    $this->assertThing('foobar', 'MISS', 'MISS');
    $this->assertThing('foobar', 'HIT', 'HIT');
  }

  protected function assertThing($title, $dpc, $pc) {
    $this->drupalGet('/some/path');
    $this->assertRaw($title);
    $this->assertCacheTags(['node:1']);
    $this->assertSame($this->drupalGetHeader('X-Drupal-Dynamic-Cache'), $dpc);
    $this->assertSame($this->drupalGetHeader('X-Drupal-Cache'), $pc);
  }

}
class CacheContextsTest extends BrowserTestBase {

  // Tests that for cached pages, permission-dependent access is respected.
  public function testBlackbox() {
    $this->assertAsAdminAndAnon(TRUE, TRUE);
    Node::load(1)->setUnpublished()->save();
    $this->assertAsAdminAndAnon(TRUE, FALSE);
  }

  protected function assertAsAdminAndAnon($visible_for_admin, $visible_for_anon) {
    $this->drupalLogin($this->administrator);
    $this->assertThing($visible_for_admin);
    $this->drupalLogout();
    $this->assertThing($visible_for_anon);
  }

  protected function assertThing($is_visible) {
    $this->drupalGet('/some/path');
    $is_visible
      ? $this->assertRaw('initial title')
      : $this->assertNoRaw('initial title');
  }

}
class CacheContextsTest extends BrowserTestBase {

  // Tests that for cached pages, permission-dependent access is respected.
  public function testWhitebox() {
    $this->assertAsAdminAndAnon(TRUE, TRUE);
    Node::load(1)->setUnpublished()->save();
    $this->assertAsAdminAndAnon(TRUE, FALSE);
  }

  protected function assertAsAdminAndAnon($visible_for_admin, $visible_for_anon) {
    $this->drupalLogin($this->administrator);
    $this->assertThing($visible_for_admin, 'MISS', NULL);
    $this->assertThing($visible_for_admin, 'HIT', NULL);
    $this->drupalLogout();
    $this->assertThing($visible_for_anon, 'MISS', 'MISS');
    $this->assertThing($visible_for_anon, 'HIT', 'HIT');
  }

  protected function assertThing($is_visible, $dpc, $pc) {
    parent::assertThing($is_visible);
    $this->assertCacheContexts(['user.permissions']);
    $this->assertSame($this->drupalGetHeader('X-Drupal-Dynamic-Cache'), $dpc);
    $this->assertSame($this->drupalGetHeader('X-Drupal-Cache'), $pc);
  }

}

?