Druxt 0.25.0; caching and performance buffs
Druxt is the Fully Decoupled Drupal framework for Nuxt 2, and 0.25.0, the first Druxt release since 2023, makes it ask Drupal a lot less.
On druxtjs.org, the Druxt docs site, a page render made 17 requests to Drupal. On 0.25.0 it makes 11, and with content caching on it makes none, until Drupal's lifetime for the page runs out. The Nuxt server keeps what Drupal sends for as long as Drupal's Cache-Control allows, and a webhook clears it when content changes.
Separately, the first-load flicker is gone, and with schema.refresh on, a change in Drupal's Manage display shows up without a rebuild, which is the part I have wanted for years. The upgrade command is near the end.
Asking Drupal once
0.24.0 made 17 calls a page, and six of them were the JSON:API index and the menus, asked again on every page. The index is Drupal's map of its API, and Druxt reads it to find everything else. Two of the six were duplicates within one render, and they go the moment you upgrade: identical requests now share one fetch, and the menu cache keys on the query it sends.
The other four are kept on the Nuxt server between requests. Drupal's Cache-Control decides how long, ttl can only shorten it, and when in doubt nothing is kept. The caching guide has the full rules. Across a 24-page crawl, that is 144 index and menu requests down to none once the first page has been rendered.
A signed-in request never fills it: an Authorization header, basic auth or a Drupal session cookie skips it, and an analytics cookie doesn't count.
Turning it on
Index and menu caching is on by default in production builds. Drupal needs one setting, the page cache max-age, which ships at 0. It applies to anonymous responses across the site, so browsers and CDNs will hold pages for five minutes too. Check Drupal itself rather than a CDN in front of it:
drush -y config:set system.performance cache.page.max_age 300
drush -y config:export
curl -sI https://cms.example.com/jsonapi | grep -i cache-control
# cache-control: max-age=300, publicIf you have turned CORS on in Drupal's services.yml, allow one origin, or * without credentials. Two or more origins, or an origin pattern, add Vary: Origin to every response, and nothing gets cached. Credentials are fine with one origin, so a preview setup like Consumer Node Preview's keeps caching, and an editor's signed-in preview skips the cache anyway.
Caching content
The index and menus are cached by default. Set cache.content and the same cache keeps everything else Druxt fetches from Drupal: entities, collections, views and route lookups.
export default {
druxt: {
cache: { content: true, secret: process.env.DRUXT_CACHE_SECRET },
},
}On a local copy of druxtjs.org, a warm docs page went from 11 calls to Drupal to none. A cold crawl, every page visited once, went from 438 calls on 0.24.0 to 157, a third of them on the first page, where schema refresh reads Drupal's displays. Only successful anonymous responses are kept, so a signed-in visitor still reaches Drupal, and a page published after someone saw its 404 shows straight away.
Content changes far more often than menus, so pair it with the clear from Drupal, below. Each cache area holds at most 50 MB of stored JSON per consumer by default, set by cache.maxSize, and can take about three times that in memory, so size it against your Nuxt process.
Clearing it from Drupal
Five minutes is a long time to wait for a menu edit, so Drupal can clear the cache the moment content changes. Give Druxt a secret:
export default {
druxt: {
cache: { secret: process.env.DRUXT_CACHE_SECRET },
},
}A server-rendered production build then has a clear endpoint, POST /_druxt/cache/clear, locked to the secret in an X-Druxt-Secret header. Each Nuxt process keeps its own cache, so a site running several clears each one. List Druxt, or a module that adds it such as druxt-site, under modules rather than buildModules, or the endpoint answers 404 under nuxt start.
On druxtjs.org, Drupal's Purge module sends that POST whenever a cache tag is invalidated, so a saved menu link reaches the frontend on the next request. It clears Druxt's cache only, so a CDN in front of Drupal needs its own purger, and a drush cr doesn't send it.
drush -y pm:install purge purge_ui purge_purger_http \
purge_queuer_coretags purge_processor_lateruntime
drush p:purger-add httpbundled # note the purger ID it prints
# At Configuration > Development > Performance > Purge, set the purger to
# Nuxt's host and port, path /_druxt/cache/clear, method POST,
# invalidation type tag, and a header X-Druxt-Secret.$config['purge_purger_http.settings.PURGER_ID']['headers'][0]['value'] = getenv('DRUXT_CACHE_SECRET');curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H "X-Druxt-Secret: your-secret" http://nuxt:3000/_druxt/cache/clearEvery tag invalidation clears the whole cache, which is blunt but cheap. The caching guide has the details.
Display changes without a rebuild
Druxt builds its schemas, which fields a component renders and in what order, from Manage display at build time, so moving a field meant a rebuild. With schema.refresh on, the server regenerates them after each clear: move a field, save, and the next page has it.
export default {
modules: ['druxt-site'], // or druxt-schema, not buildModules
druxt: {
cache: { secret: process.env.DRUXT_CACHE_SECRET },
schema: { refresh: true },
},
}The first page after a clear pays for it, with about two dozen configuration reads on druxtjs.org, and every page after that is back to normal. The server generates with the build's access to Drupal, so those credentials need to be in production too. A static site can run the same refresh from its own Node server with createSchemaRefresh, which the caching guide covers.
What it saves
Over a network that adds up. On a local copy of druxtjs.org, alternating the versions page by page, 0.25.0 rendered warm pages nearly a third faster than 0.24.0 in all 66 paired visits, and in about a sixth of 0.24.0's time with content caching on. Inside a hosting cluster, where Drupal is a couple of milliseconds away, the time saved is small, and the win is Drupal answering fewer requests. Put a proxy in front of your own Drupal and count: how many times does your site ask the same question?
Drupal is getting quicker too. ptmkenny's core patch in #3626255 reads JSON:API's cached normalizations in bulk. On the docs collection behind druxtjs.org's sitemap, with normalizations warm, it took 1301 database queries down to 77 with identical responses. It needs reviews, and the issue covers running it on 11.x.
Also in 0.25.0
- No more first-load flicker. Druxt components no longer fetch their data again when a production build hydrates, so any
components:extendhook you added to hide it can go. - Pages don't jump on a refetch, because components keep their content while they fetch.
- Views follow the language you asked for, not the site default.
- A 403 or 404 from a route renders through Nuxt's own error page.
- Vue and Vuex are peer dependencies, so a site uses its own copy, and a mismatched second copy can't stop Nuxt at startup.
Upgrading
The packages version together, so move them in one go:
# Drop any druxt package your site doesn't use
npm install druxt@^0.25.0 druxt-site@^0.15.0 druxt-router@^0.32.0 \
druxt-entity@^0.29.0 druxt-menu@^0.22.0 druxt-blocks@^0.18.0 \
druxt-views@^0.23.0 druxt-breadcrumb@^0.18.0 druxt-schema@^0.12.0 \
axios@^0.34.0
# Still on Vue 2.6? Add vue@^2.7.14 vuex@^3.6.2
# Using druxt-auth? Take it to 0.5.1 in the same install.The deprecated DruxtField components are off unless you set druxt.entity.components.fields: true, and a consola 2.15.3 pin can go. Wrapper components no longer render module data as HTML attributes, so entity="[object Object]" is gone from your markup. Wrappers still read it from $attrs. The rest is in the changelog on druxtjs.org.
Why 0.25.0, and not Nuxt 4?
Nuxt 2 is end of life, so it is a fair question. Druxt had years of improvements and fixes sitting unreleased, and I would rather finish them than carry them across half done. 0.25.0 gets that work out, and gives the 2.0.0 port a stable, fast baseline to start from. Get it right on the stack it runs on today, then upgrade, then add new features, then polish.
There is one more 0.x release planned before 1.0.0. 0.26.0 is mostly waiting on a decoupled router patch, so a site whose default language has no URL prefix stops redirecting its front page to /en, plus whatever other multilingual fixes turn up before then. Then 1.0.0, the last Druxt on Nuxt 2, with Druxt 2.0.0, the Nuxt 4 rewrite under @druxt, on track for a first beta alongside it, building on the caching model 0.25.0 settled on.
Experimenting with WebSockets
Something to play with while you wait. Umami Go is a multiplayer card game on the Umami demo, dealt from the magazine's recipes and rendered by Druxt. It is also where you can try the upcoming Druxt WebSockets module, still in development: edit a recipe in Drupal, and it changes in every hand holding it.
Druxt 1.0.0 and 2.x move as fast as sponsorship lets them.