01 Oct 2026

feedDrupal.org aggregator

The Drop Times: Bento Showcase Brings Visual Hierarchy to Drupal Views

A Drupal View does not have to end as a row of equal cards. Bento Showcase tests how far sorting, promotion, and image treatment can carry a structured content collection toward a composed page.

01 Oct 2026 11:01am GMT

Nuvole: Cutting Drupal's site:install time from 20 minutes to 90 seconds with an AI coding agent

A large multilingual Drupal site took almost 20 minutes to self-install, which made every CI run painfully slow. Here's how a PHP profiler and an AI coding agent got it down to 90 seconds.

"We'll add the tests later" only works if the tests are fast enough that nobody is tempted to skip them. On one of our large multilingual Drupal projects, they weren't.

The site has content in 100 languages, around 200 modules, 400+ configured fields, and up to 5,000 config files once translations are counted in. Running drush site:install against that configuration took close to 20 minutes. Add Docker startup and the actual end-to-end test run on top, and a single CI pipeline run could easily take an hour. Copying the production database into CI wasn't an option, so a full config-driven install was the only realistic way to get a clean site to test against for every change.

An install that slow doesn't just cost CI minutes, it costs feedback loops. So we set out to find out exactly where the 20 minutes were going, fix what we could, and see how AI could shorten the loop of measuring, fixing and re-measuring.

Setting up a profiler

The first step was getting visibility into what Drupal was actually doing during install. We used SPX, a lightweight PHP profiler that's easy to drop into a Docker-based PHP image alongside xdebug or blackfire alternatives, without much of the performance overhead those carry.

Once enabled, SPX gives you a web UI to browse individual profiles and see wall time, memory and call counts per function:

Screenshot of the PHP-SPX web configuration panel

And for any profiled request, a flame-graph-style view of the whole call stack, with a sortable table of the most expensive functions underneath:

Typical SPX profile report, showing a flame graph and a sortable table of function call times

That's already useful for a single page load. The interesting part is what happens when you point the same tooling at an AI coding agent.

Giving the coding agent access to the profiler

SPX ships an MCP server extension, SPX-MCP, which exposes recorded profiles through a query-like interface instead of a browser UI. Wiring it into an agent's MCP configuration means the agent can list profiles, drill into a specific request, and pull out the functions responsible for the most time, without a human first opening a flame graph and squinting at it.

In practice, that turns "let me go look at a profile and tell you what's slow" into a two-line prompt:

Screenshot of an AI agent drilling into the costliest functions of a homepage request

The agent found the three most recent profiles, correctly noticed they were regular page requests rather than the site:install run we actually cared about, and then, once pointed at the right report, broke a 1.44s homepage request down into HTML rendering, Twig compilation, class loading, cache reads and database time, each with concrete function names attached. That's the same triage a developer would do manually, just a lot faster to get to.

The problem with profiling a 20-minute install

Profiling a single page request is straightforward. Profiling a 20-minute drush site:install is a different story: turning SPX on for the whole run produced one profile with almost 590 million calls, weighing about 8GB on disk. That's too large to open in the web UI, and too large for an agent to reason about as a single blob.

The fix was to split it into one profile per install stage instead of one profile for the whole run. Drupal's installer runs through a fixed sequence of tasks (install_select_language, install_config_import_batch, install_import_translations, and so on), and there's a single place in core, install_run_tasks(), where each task starts and finishes. A small patch there starts and stops an SPX profile around each task and tags it with the task name as metadata:

--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ function install_run_tasks(&$install_state, ?callable $callback = NULL) {
     $task = array_shift($tasks_to_perform);
     $install_state['active_task'] = $task_name;
     $original_parameters = $install_state['parameters'];
+    if (function_exists('spx_profiler_start')) {
+      spx_profiler_start();
+      spx_profiler_full_report_set_custom_metadata_str(json_encode(['task' => $task_name]));
+    }
     $output = install_run_task($task, $install_state);
     ...
+    if (function_exists('spx_profiler_stop')) {
+      spx_profiler_stop();
+    }
   } while (!$finished);

That turned one unmanageable profile into a dozen small ones, each scoped to a single install task, each cheap enough for the agent (or a human) to load and query directly. For the rare case of a profile that's still too big, there's also SPXQ, a small command-line viewer for browsing multi-gigabyte SPX reports without loading the whole thing into memory or a browser tab.

Finding the bottlenecks

With per-task profiles available, the workflow became a loop: profile a task, find the most expensive function, patch it, install again, and check the diff in timing. Doing that by hand, task by task, function by function, is tedious, exactly the kind of repetitive, well-defined work an AI coding agent is good at once it has direct access to the profiling data instead of having to be told what's in it.

One of the clearest examples was the install_config_import_batch task, which turned out to spend the majority of its time in EntityStorageBase::doPostSave():

SPXQ terminal view of install_config_import_batch, showing doPostSave at 52.57% of wall time

Digging into that call chain showed that saving a single field config was re-triggering hooks and cache invalidation for every entity type's base field definitions, even when only one bundle had actually changed, and it was doing that for every field, of which there were hundreds. After each fix, the agent could re-run the install, pull the new profile, and confirm the improvement against the previous run.

What actually moved the needle

After several rounds of this profile-fix-reprofile loop, here's what made the biggest difference:

  1. Cache collection names. StorageComparer::getAllCollectionNames() was rescanning the sync directory to find language collections on every one of ~2,800 config items during import, even though the answer never changes. Computing it once per import and reusing it cut that step by 27.6%.

  2. Cache translatable default config. LocaleConfigManager::getTranslatableDefaultConfig() rebuilt a typed-config wrapper once per language, even though the shipped default config is identical across languages. Caching it by config name cut this by 53.5%.

  3. Cache isSupported(). The sibling of the fix above, doing two redundant config reads per language for an answer that doesn't depend on the language. Caching it removed another 5.4%.

  4. Scan the translation directory once. The same, usually-empty translation folder was being scanned for every project/language pair, up to 1,580 times. Listing it once and matching filenames against that list brought the whole step down to about 6ms.

  5. Skip no-op config saves. LocaleConfigManager::updateConfigTranslations() was saving configuration even when nothing had actually been translated, reloading language override objects for all languages on every save. A simple !empty($processed) guard removed 237 unnecessary saves.

  6. Stop force-rebuilding a cold field map. After every container rebuild, FieldDefinitionListener::onFieldDefinitionCreate() was forcing a full field map rebuild for every single field created during install. Skipping that while the map is still cold cut getBaseFieldDefinitions() calls by 76%, with no change to the resulting field map.

  7. Narrow the cache clear on field save. This was the single biggest win. Saving one field config was wiping every entity type's base field definitions and triggering a full plugin rediscovery. Clearing only the affected bundle's field definitions during install cut this step by 18%, verified as producing byte-identical definitions across 37 entity/bundle combinations.

  8. No-op the translation status key/value store during install. A small wrapper turns locale.translation_status lookups into an in-memory no-op during install (and passes through normally afterwards), avoiding repeated database hits for a value that's meaningless mid-install.

  9. Disable remote translation lookups for CI installs. During an automated test install there's no reason to reach out to the localization server, so translation.import_enabled and related settings are simply turned off for that environment. This one is a CI-only optimization, not something you'd want in production.

  10. Skip irrelevant entity hooks during install. Some entity_update/entity_insert hooks have no meaning during install or fixture loading. Guarding them behind $entity->isSyncing() avoided pointless work; for example, the Search API module's _search_api_view_crud_event() handler.

None of these are exotic. Individually, most are the kind of "don't redo work you already did" fix any Drupal developer would recognize once pointed at it. What changed was how quickly they could be found and verified: instead of manually reading flame graphs and re-running a 20-minute install to check each guess, the agent could query a profile, propose a targeted fix, and confirm the improvement against the previous measurement, over and over.

The result

After all of it: 1 minute 30 seconds, down from roughly 19 minutes 30 seconds. A 13x improvement on the number that mattered most for us, the total install time inside the CI pipeline.

Not every fix here is something we'd apply blindly to every project. Some, like skipping remote translation downloads, only make sense for a throwaway test install. Others touch Drupal core behaviour around cache invalidation and are being submitted as proper core patches with issues on drupal.org, since they need review and testing well beyond "it worked for us".

Bonus

Both the SPXQ profile viewer and a reusable "profile, analyze, fix, validate" skill for coding agents are public:

Tags: Drupal Planet, Drush, Performance, AI, Drupal, DrupalCon

01 Oct 2026 11:00am GMT

Jacob Rockowitz: Vibing Drupal: What’s your take on the /grill-me skill?

After watching the Driesnote (aka the keynote) from DrupalCon Rotterdam, I was inspired and overwhelmed by everything happening in Drupal that I need to catch up on.

Immediately, I should help get the new Tool API working with the Webform module, but first I need to work through the Webform module issue backlog. Second, I need to learn more about Drupal Canvas and start defining a roadmap for my organization to move to Drupal Canvas. Lastly, I have no experience with ECA, and now there's FlowDrop to look at too.

My new AI reality is that I'm generally no longer overwhelmed by new technical challenges because I leverage AI to help me learn and plan how to evolve my knowledge base and the applications I support. I've used AI enough, building and maintaining a Drupal site and its modules, to know just how capable today's frontier models are. At the same time, I struggle to communicate with AI and get it to understand my requirements to achieve my goals. During these struggles, I get slapped in the face with AI slop.

After deleting most of my agent skills and starting over, I began experimenting with one of the most popular agent skills, /grill-me, which proved to be a game changer.

The /grill-me skill was created by Matt Pocock and is mentioned in many Reddit threads. I was not sold on even trying it until I listened to Read More

01 Oct 2026 8:52am GMT