Skip to content
HELiX

Performance Overview

apps/docs/src/content/docs/drupal/performance/overview Click to copy
Copied! apps/docs/src/content/docs/drupal/performance/overview

HELiX components are ES modules built on Lit. Loading them efficiently in Drupal requires understanding where the weight sits, how Drupal’s library system interacts with ES modules, and which strategies match your site’s deployment constraints.


Bundle Size Reference — @helixui/library@3.9.0

Section titled “Bundle Size Reference — @helixui/library@3.9.0”

These sizes are gzipped. Raw sizes are approximately 2.5–3× larger.

Load strategyApproximate gzipped sizeWhen to use
Full bundle (dist/index.js)~38 KBPrototypes, sites using 8+ components
Per-component (e.g., hx-button)3–6 KB per componentProduction sites, page-specific loading
Lit runtime alone~12 KBShared dependency baseline
Tokens CSS (@helixui/tokens@3.9.0)~4 KBAlways load separately

The full bundle CDN URL is:

https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js

Per-component CDN pattern:

https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-button/index.js
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-card/index.js
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-text-input/index.js

Full bundle — simplest, highest initial cost

Section titled “Full bundle — simplest, highest initial cost”

Load all components at once from a single URL. Drupal attaches one library globally.

mytheme.libraries.yml
helix:
version: 1.1.2
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js:
type: external
preprocess: false
attributes:
type: module
crossorigin: anonymous
mytheme.info.yml
libraries:
- mytheme/helix

Tradeoff: Every page loads ~38 KB regardless of which components appear. Acceptable for component-dense applications. Unacceptable when most pages use only 1–2 components.

Per-component loading — minimal, page-specific

Section titled “Per-component loading — minimal, page-specific”

Define a library per component and attach only what each template needs.

mytheme.libraries.yml
helix-button:
version: 1.1.2
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-button/index.js:
type: external
preprocess: false
attributes:
type: module
crossorigin: anonymous
helix-card:
version: 1.1.2
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-card/index.js:
type: external
preprocess: false
attributes:
type: module
crossorigin: anonymous
helix-text-input:
version: 1.1.2
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-text-input/index.js:
type: external
preprocess: false
attributes:
type: module
crossorigin: anonymous

Attach in templates or preprocess:

{# node--article--teaser.html.twig #}
{{ attach_library('mytheme/helix-card') }}
{{ attach_library('mytheme/helix-button') }}

Tradeoff: More library definitions to maintain. HTTP/2 makes many small parallel requests cheap. Ideal for sites where different sections use different component sets.


Every HELiX component depends on Lit. When per-component files are loaded separately, each would normally include its own copy of Lit — duplicating ~12 KB across components.

Approach 1: Use the shared runtime entry point

Section titled “Approach 1: Use the shared runtime entry point”

jsDelivr serves Lit as a shared module import. Because browsers cache ES modules by URL, the same Lit bundle is reused across all component imports when they reference the same URL:

https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/lit-runtime.js

Define it as a dependency:

mytheme.libraries.yml
helix-runtime:
version: 1.1.2
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/lit-runtime.js:
type: external
preprocess: false
attributes:
type: module
crossorigin: anonymous
helix-button:
version: 1.1.2
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-button/index.js:
type: external
preprocess: false
attributes:
type: module
crossorigin: anonymous
dependencies:
- mytheme/helix-runtime
helix-card:
version: 1.1.2
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-card/index.js:
type: external
preprocess: false
attributes:
type: module
crossorigin: anonymous
dependencies:
- mytheme/helix-runtime

Drupal’s library dependency system ensures helix-runtime loads only once even if multiple component libraries depend on it.

If you build from npm (@helixui/library), your bundler can extract Lit as a shared chunk:

vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('lit') || id.includes('@lit')) {
return 'lit-runtime';
}
},
},
},
},
};

This produces dist/lit-runtime.js and individual component files that import from it, achieving the same deduplication locally.


Drupal’s built-in JS aggregation concatenates files — a technique incompatible with ES modules. You must disable preprocessing for any HELiX library:

helix-button:
js:
dist/hx-button.js:
preprocess: false # REQUIRED — aggregation breaks ES module imports
minified: true
attributes:
type: module

Without preprocess: false, Drupal concatenates the module file with other scripts, breaking the import/export syntax at runtime.


HTTP/2 multiplexing allows many files to download in parallel over a single connection. This eliminates the HTTP/1.1 penalty for multiple requests and makes per-component loading viable without concatenation.

On HTTP/2 infrastructure, 10 component files of 4 KB each typically load faster than one file of 40 KB because:

  • Downloads start simultaneously
  • Each file is individually cacheable
  • Browser caches individual components when only one changes

Verify your Drupal server uses HTTP/2:

Terminal window
curl -I --http2 https://your-site.com | grep -i "HTTP/"

Reduce waterfall latency by declaring component files in the <head> before the browser parses templates:

/**
* Implements hook_page_attachments().
*/
function mytheme_page_attachments(array &$attachments): void {
// Preload components used on every page.
$attachments['#attached']['html_head_link'][][] = [
'rel' => 'modulepreload',
'href' => 'https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/lit-runtime.js',
'crossorigin' => 'anonymous',
];
}

Drupal’s render cache stores rendered HTML for component-heavy regions. Tag caches with entity cache tags so they invalidate correctly:

function build_article_card(NodeInterface $node): array {
return [
'#theme' => 'node',
'#node' => $node,
'#view_mode' => 'teaser',
'#attached' => ['library' => ['mytheme/helix-card']],
'#cache' => [
'keys' => ['article-card', $node->id()],
'contexts' => ['user.roles'],
'tags' => $node->getCacheTags(),
'max-age' => Cache::PERMANENT,
],
];
}

jsDelivr URLs include the exact version number (@1.1.2). Browsers cache these with long-lived headers. A component update requires a new library version in Drupal’s YAML — the URL changes, bypassing browser cache.

# Old version — cached in browsers
helix-button:
version: 1.1.1
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-button/index.js: ...
# New version — new URL, fresh download
helix-button:
version: 1.1.2
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-button/index.js: ...

If serving local files, increment the version key to bust Drupal’s cache-busting query string (?v=1.1.2):

helix-button:
version: 1.1.2 # Drupal appends ?v=1.1.2 to the script URL
js:
dist/hx-button.js:
preprocess: false
attributes:
type: module

MetricHELiX ConsiderationMitigation
LCP (Largest Contentful Paint)Components upgrade asynchronously — LCP element may be unstyled brieflyUse :not(:defined) skeleton styles; prioritize loading components used above fold
INP (Interaction to Next Paint)Component event handlers are attached after upgradeLit component upgrades are fast; avoid blocking main thread in connectedCallback
CLS (Cumulative Layout Shift)Components with unknown dimensions before upgrade shift layoutSet explicit dimensions with CSS on the host element or use contain: layout
/* Set stable dimensions before the component upgrades */
hx-card {
display: block;
min-height: 200px;
}
hx-button {
display: inline-block;
min-width: 80px;
min-height: 40px;
}

HELiX components include fouc.css distributed with @helixui/tokens. Load it before component scripts:

mytheme.libraries.yml
helix-fouc:
css:
theme:
https://cdn.jsdelivr.net/npm/@helixui/tokens@3.9.0/dist/fouc.css:
type: external
js: {}

Or use the :not(:defined) pattern:

hx-button:not(:defined),
hx-card:not(:defined),
hx-text-input:not(:defined) {
visibility: hidden;
}

Load component bundles only on routes that need them:

/**
* Implements hook_preprocess_page().
*/
function mytheme_preprocess_page(array &$variables): void {
$route_name = \Drupal::routeMatch()->getRouteName();
$route_library_map = [
'entity.node.canonical' => 'mytheme/helix-content',
'my_module.contact_form' => 'mytheme/helix-forms',
'view.articles.page_1' => 'mytheme/helix-card',
];
foreach ($route_library_map as $route => $library) {
if (str_starts_with($route_name, $route)) {
$variables['#attached']['library'][] = $library;
$variables['#cache']['contexts'][] = 'route.name';
}
}
}

Before deploying a Drupal site with HELiX components, verify:

  • preprocess: false on all ES module library entries
  • type: module attribute on all script entries
  • Full bundle not loaded on pages that use only 1–2 components
  • Shared Lit runtime loaded as a library dependency (not duplicated per component)
  • HTTP/2 enabled on the production server
  • drush cr run after any .libraries.yml change
  • FOUC mitigation CSS loaded before component scripts
  • Render cache tags set correctly on component-heavy regions
  • CDN preconnect added for jsDelivr if using CDN delivery