Think of a busy restaurant kitchen that re-cooks a dish from scratch for every single order, even when fifty people ordered the exact same thing. That's what an uncached form does every time it renders — Drupal rebuilds every render array element from nothing on every request. Cache tags and contexts are how you tell the kitchen "keep a batch of this ready, and here's exactly when you need to throw it out and start fresh."
What you'll learn in this lesson
- The real difference between a cache tag and a cache context, and why forms need both concepts
- How to attach
#cachemetadata to a form render array - Why forms are usually only partially cacheable, not wholesale cacheable
Cache tags vs. cache contexts — two different jobs
These two terms get confused constantly, so it's worth being precise: a cache tag answers "when does this cached data go stale?" — it's an invalidation trigger, like node:42, that Drupal fires whenever that specific thing changes. A cache context answers a completely different question: "does this cached data even mean the same thing for every visitor?" — it's a variation key, like user or languages:language_interface, that tells Drupal to keep separate cached copies per context value rather than serving one visitor's cached output to another.
$build['cache_demonstration'] = [
'#markup' => $this->t('Hello @name, welcome to the #cache example.', ['@name' => $this->currentUser->getAccountName()]),
'#cache' => [
'contexts' => [
'user',
],
],
];
This is the real #cache pattern from Drupal's own Render Example module. Notice the string embeds the current user's name — without the user context declared, the first visitor's name would get baked into a cached copy that every subsequent visitor would then see. Declaring the context tells Drupal's render cache: "store a separate copy of this element for every distinct user."
Why a whole form is rarely one cacheable unit
Here's the part that trips people up: you almost never put #cache on the outer $form array itself. A form contains a CSRF token, default values pulled from the current user's data, and validation state — all of that is inherently request-specific and uncacheable. What you actually cache are the static, expensive-to-build pieces inside the form: a long list of options fetched from an external API, a rendered help block, a complex nested fieldset whose content doesn't depend on who's looking at it.
$form['expensive_options_list'] = [
'#type' => 'select',
'#title' => $this->t('Choose a region'),
'#options' => $this->buildExpensiveOptionsList(),
'#cache' => [
'tags' => ['region_list'],
'contexts' => ['languages:language_interface'],
],
];
Tag it with region_list so that whatever code updates the region list also calls Cache::invalidateTags(['region_list']) to bust this exact cached fragment — no need to clear the whole page cache for an unrelated change elsewhere on the site.
$form_state or reflects a just-submitted value. Anything derived from the current request's mutable state is, by definition, not safe to reuse for the next request — cache only what would legitimately look the same to the next visitor who has the same cache context values.Quick check: you cache a "recommended products" fieldset inside a form with
'contexts' => ['user.roles']. A site has 40,000 individual users but only 4 roles. How many separate cached copies of that fieldset will Drupal end up storing? (Answer: at most 4 — one per unique combination of roles, not one per user. This is exactly whyuser.rolesis far cheaper than the more specificusercontext when role is all that actually matters.)
Key takeaways
- Cache tags control when cached data expires; cache contexts control how many variants of it get stored.
- Almost never cache the outer
$formarray as a whole — target the specific expensive, request-independent pieces inside it. - Always pair a cache tag with the code path that actually invalidates it, or the cached data will silently go stale.
- Prefer the least specific cache context that's still correct —
user.rolesoveruserwhenever role, not identity, is what actually varies the output.
Coming up next
Caching handles data that's expensive to compute but doesn't change often. The next lesson tackles a different performance lever entirely: trimming down the form itself so there's simply less to build, render, and submit in the first place — Optimizing Form Element Rendering.