Making Drupal fly

The fastest Drupal ever is near!


The fastest ever Drupal?

Are you serious?

You are kidding right?

But I thought Drupal 8 was slow!!!11111

Everyone told me that!

So it must be TRUE!

And besides that, page cache was already in Drupal 7!

Page Cache really does not count!

  • ( Except it does, as Wim will show you later. )

So let's take a look ...

... at a short demo ...

... to show you ...

Demo

What you just saw ...

... is part of the mission ...

... to make the whole web fast.

Make the whole web fast

  • A challenge by Wim Leers.
  • http://calendar.perfplanet.com/2013/making-the-entire-web-fast/
  • Everyone profits from this!

Ambitious goal, so lets see what "fast" means:

  • Great User Experience, pages load fast for 1 user
  • Great User Experience, pages load fast for [n] concurrent users
  • Time-to-First-Byte + Asset loading + Rendering + JS exec time == minimal

How do you achieve that?

  • SIMPLE!
  • Use static HTML pages
  • Use no CSS
  • Use no Javascript
  • Use no images
  • Just text and links!

Seriously?

No, of course not!

There is some truth to that, though!

Performance Optimization means:

Optimize to do:

  • As little work as possible!
  • As little resources as possible!

So for the "critical path" the process is:

Can we:

  • ... avoid doing the work at all?
  • ... avoid doing the work during the critical path?
  • ... cache it permanently?
  • ... cache it temporarily?
  • ... defer executing it after the main content? (Big Pipe)

So, remember ACD:

Avoid, Cache, Defer

Caching has problems

Content should:

  • ... be as current as possible.
  • ... have a high cache hit ratio.
  • ... have low cache invalidation complexity.

Choose 2!

You can't have all three!

Examples for low complexity cache invalidation:

Time-Based Invalidation

  • Cache pages unconditionally for a year, content is not really current, but cache hit ratio is great.
  • Cache pages never, content is always current, but cache-hit ratio is 0%.
    • A lot of Drupal 7 sites operate that way right now :(
  • Cache pages for e.g. 6 hours, content is quite current and cache hit ratio is acceptable.

Examples for low complexity cache invalidation:

Clear All Invalidation

  • Whenever a page changes, clear the whole page cache.
  • ⇒ Drupal 7's page cache operates like this by default.

Examples for high complexity cache invalidation (D7 contrib):

Clear only what has changed

  • e.g. expire / varnish module in D7.
  • Needs to purge / expire all URLs that contain content from anything and needs to track that.
  • ⇒ High invalidation cost

Drupal 8 chose high-complexity

  • Content invalidated instantaneously
  • Cached permanently
  • Solution: Cache Tags (explained later)

Caching has more problems

Content should:

  • ... be varied by a user / group / special permission / phase of the moon (granularity).
  • ... have a high cache hit ratio.
  • ... have low complexity (logic needed to ensure caches are granularly varied).

Choose 2!

You can't have all three!

( What again? )

Drupal 8 chose high-complexity

  • Everything declares what it varies by.
  • This allows caching authenticated user content securely.
  • Solution: Cache Contexts (explained later) + Placeholders

Wait a moment!

What about KISS?

(Keep It Simple, Stupid!)

Counter Question:

Who of you uses a database?

Databases are actually "beasts of complexity"

... but you don't see any of that!

... you just give hints and the database does its magic.

Drupal 8 makes it as simple as possible for you!

  • Huge opportunity!
  • Drupal 8 does all the caching logic internally.
  • If you give Drupal information, then it can be smarter about its decisions.

Drupal 8 formalizes those things in a "language" to make your site fast.

Drupal 7 can use the same "language" — once it is finalized in Drupal 8.

  • render_cache 7.x-2.x module
  • service_container — using the same code base as Drupal 8
  • [ developed in parallel, but 7.x frozen atm. ]

But how do I give this information?

Wim will show you now!

The thought process

The theory of how we make Drupal fast

Dependencies, dependencies, dependencies!

  • Drupal 7 didn't track any dependencies
  • e.g. drupal_add_css(), drupal_add_js()
  • ⇒ impossible to cache
  • #attached asset libraries solve that

Dependencies, dependencies, dependencies!

  • Drupal 7 didn't track any dependencies
  • e.g. url()'s output depended on:
    • <front> configuration
    • HTTPS configuration
    • clean URL configuration
    • current site in multisite
    • current host name
    • path processing
      • negotiated interface language
      • negotiated URL language
  • ⇒ impossible to cache invalidate
  • … yet many of us did it anyway!

Correct invalidation in Drupal 8

  • Cache tags (data dependencies)
  • Cache contexts (context dependencies)
  • Cache max-age (time dependencies)

+

Cacheability bubbled during rendering!

In practice

Try to make this thought process a habit:

1.

I'm rendering something. That means I must think of cacheability!

2.

Is this something that's expensive to render, and therefore is worth caching?

↪︎ If "yes": cache keys

$build['#cache']['keys'] = ['node', 5, 'teaser'];

3.

Does the representation of the thing I'm rendering vary per combination of permissions, per URL, per interface language, per … something?

↪︎ If "yes": cache contexts

$build['#cache']['contexts'][] = 'user.permissions';
$build['#cache']['contexts'][] = 'url';

~ HTTP's Vary header

4.

What causes the representation of the thing I'm rendering become outdated?

↪︎ If "yes": cache tags

$build['#cache']['tags'][] = 'node:5';
$build['#cache']['tags'][] = 'user:3';
$build['#cache']['tags'][] = 'taxonomy_term:23';

5.

When does the representation of the thing I'm rendering become outdated?
↪︎ If "yes": cache max-age
$build['#cache']['max-age'] = Cache::PERMANENT;

~ HTTP's Cache-Control: max-age header

To make it easier:

Renderer::addCacheableDependency($dependency)

$site_config = $this->config->get('system.site');

$build = [
  '#markup' => t('Welcome to @site!', $site_config->get('name')),
];
$this->renderer->addCacheableDependency($site_config)

If Drupal pages were ships…

(Drupal rendering a page ~ building a ship)

… then this could be Drupal 8…

Assembled from components. Clear dependencies.

… and this would be Drupal 7

Assembled from seemingly random pieces. But it is a boat!

Pitfalls + Scenarios

Depending on configuration?

Don't forget to add its cache tags!

Renderer::addCacheableDependency($build, $config)

Depending on an entity?

Don't forget to add its cache tags!

Renderer::addCacheableDependency($build, $entity)

Depending on a field of an entity?

Don't forget to add the entity's cache tags!

Renderer::addCacheableDependency($build, $entity)

Uncacheable data?

Don't forget to set max-age to zero!

$build['#cache']['max-age'] = 0;

Manually rendering a link?

Either:

  • Use Twig:
    {{ link(something.title, something.url) }}
  • Use #type => link:
    $build['link'] = [
      '#type' => 'link',
      '#title' => 'Drupal',
      '#url' => Url::fromUri('https://drupal.org'),
    ];

Scenario: vary by cookie

$args = ['%username' => $_COOKIE['username']];
$build['#markup'] = $this->t('Hi, %username', $args);

$build['#cache']['contexts'][] = 'cookies:username';

Scenario: always vary by a 'device' cookie

In sites/yoursite.com/services.yml:

parameters:
  renderer.config:
    required_cache_contexts:
      # The two default required cache contexts.
      - 'languages:language_interface'
      - 'theme'

      # The one we added!
      - 'cookies:device'

Hope you liked it!

Questions?

wimleers.com/talk/making-drupal-fly-fastest-drupal-ever-near


Docs

d.o/developing/api/8/cache/contexts

d.o/developing/api/8/render/arrays/cacheability


BigPipe issue

d.o/node/2469431