PHPackages                             bennypowers/backlit - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. bennypowers/backlit

ActiveDrupal-module[Utility &amp; Helpers](/categories/utility)

bennypowers/backlit
===================

Server-render Lit web components from Drupal. No Node.js. No containers. Just vibes and NUL bytes.

v0.1.0(3mo ago)85[2 issues](https://github.com/bennypowers/backlit/issues)[1 PRs](https://github.com/bennypowers/backlit/pulls)MITPHPPHP &gt;=8.1CI passing

Since Mar 19Pushed 2mo ago1 watchersCompare

[ Source](https://github.com/bennypowers/backlit)[ Packagist](https://packagist.org/packages/bennypowers/backlit)[ RSS](/packages/bennypowers-backlit/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (2)DependenciesVersions (4)Used By (0)

Backlit
=======

[](#backlit)

[![Backlit](backlit.png)](backlit.png)

Server-render [Lit](https://lit.dev) web components from [Drupal](https://www.drupal.org/). No Node.js. No containers. No HTTP sidecar. Just a single binary that speaks NUL bytes.

Backlit hooks into Drupal's response pipeline and renders every Lit web component with [Declarative Shadow DOM](https://html.spec.whatwg.org/multipage/scripting.html#attr-template-shadowrootmode). Users see styled, laid-out content on first paint -- before any JavaScript loads. Disable JS entirely and the components still render. That is the way of the Lit.

Quick start
-----------

[](#quick-start)

```
composer require bennypowers/backlit
drush en backlit
```

That's it. Composer downloads the right binary for your platform. Drush enables the module. Every page response now gets its web components server-rendered.

If the binary download didn't run automatically:

```
cd web/modules/contrib/backlit
./scripts/download-binary.sh
```

How it works
------------

[](#how-it-works)

Backlit ships a pre-compiled Go binary that embeds a WASM module. Inside that WASM module: [QuickJS](https://bellard.org/quickjs/) running [`@lit-labs/ssr`](https://www.npmjs.com/package/@lit-labs/ssr). On startup, the binary bundles your component source files (JS or TS) with esbuild, evaluates the bundle inside QuickJS, and registers your custom elements.

When Drupal finishes rendering a page, Backlit's `SsrResponseSubscriber` intercepts the response, pipes the HTML through the binary's stdin, and reads Declarative Shadow DOM enhanced HTML from stdout. The binary uses a NUL-delimited read-loop protocol, so the WASM instance and your component definitions stay warm across renders.

```
First render:  ~350ms  (WASM cold start -- paid once per PHP-FPM worker)
Every render after:  ~0.32ms  (just pipe I/O)

```

The binary auto-detects your platform. Supported: linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64, win32-arm64. Yes, we support Windows. No, we haven't tested it. Godspeed.

Adding your components
----------------------

[](#adding-your-components)

Drop JS or TS source files into one of these locations. Backlit aggregates component files from all of them:

1. **`$settings['backlit']['components_dir']`** in `settings.php`
2. **Your active theme's `components/` directory** -- e.g., `themes/custom/my_theme/components/`
3. **Any custom module's `js/` directory** -- e.g., `modules/custom/my_components/js/`

The binary bundles your source files with esbuild at startup, resolving imports from `node_modules`. Declaration files (`.d.ts`) and test files (`.test.ts`, `.test.js`) are excluded automatically.

### What the source looks like

[](#what-the-source-looks-like)

Standard LitElement with standard imports:

```
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('my-card')
class MyCard extends LitElement {
  @property() heading = '';

  static styles = css`
    :host { display: block; border: 1px solid #ccc; border-radius: 8px; }
    #header { padding: 16px; font-weight: 600; }
    #body { padding: 16px; }
  `;

  render() {
    return html`
      ${this.heading}

    `;
  }
}
```

Then use it in any Drupal content (Full HTML format):

```

  All systems operational.

```

Components stay registered across renders -- the binary bundles and evaluates your source once, then keeps the definitions warm.

### Pre-bundled mode (advanced)

[](#pre-bundled-mode-advanced)

If you prefer to bundle components yourself, you can pass a pre-built JS bundle via `$settings['backlit']['bundle']` in `settings.php`. The binary will skip esbuild and evaluate the bundle directly.

Performance
-----------

[](#performance)

MetricValueCold start~350ms (once per PHP-FPM worker)Warm render~0.32msBinary size~9 MB (statically linked, no dependencies)Memory~20 MB per WASM instanceDependenciesZero. The binary is the dependency.For comparison, the [previous approach](https://bennypowers.dev/posts/drupal-lit-ssr/) required a Node.js container, HTTP round-trips, and ~50ms per render. The drop has moved.

Requirements
------------

[](#requirements)

- Drupal 10 or 11
- PHP 8.1+
- A server that can run a binary (so, any server)
- `node_modules` with `lit` installed (esbuild resolves imports at startup)
- No PHP extensions, no PECL, no FFI, no containers

Graceful degradation
--------------------

[](#graceful-degradation)

If the binary isn't available or fails to start, Backlit returns the original HTML unchanged. Your components will still work client-side once their JavaScript loads -- they just won't have the instant first paint from DSD. This means you can develop locally without the binary and deploy with it.

Architecture
------------

[](#architecture)

```
Browser request
       |
   Drupal renders page (Twig, etc.)
       |
   KernelEvents::RESPONSE
       |
   SsrResponseSubscriber
       |
   LitSsrRenderer::render()
       |
   proc_open() -> lit-ssr binary (Go + wazero + QuickJS + @lit-labs/ssr)
       |
   stdin: HTML\0  ->  stdout: HTML-with-DSD\0
       |
   Response sent to browser with Declarative Shadow DOM
       |
   User sees styled content immediately
   (JavaScript loads later, hydrates if needed)

```

FAQ
---

[](#faq)

**Does this work with any web component framework?**Only Lit. The SSR engine is `@lit-labs/ssr`, which understands Lit's template system. Vanilla custom elements or other frameworks would need their own SSR implementation.

**What about caching?**Backlit operates on every response. If you have Drupal's page cache enabled, the rendered HTML (with DSD) gets cached, so subsequent requests skip the binary entirely. This is the recommended setup.

**What about BigPipe?**Backlit runs in a `KernelEvents::RESPONSE` subscriber, which processes the initial HTML response before it is sent to the browser. BigPipe replaces placeholder markup with real content *after* that initial response, streaming replacement `` tags that swap in the final HTML on the client. Web components inside BigPipe-delivered placeholders will not be server-rendered by Backlit, since those fragments arrive after the response subscriber has already run. Components will still render client-side once their JavaScript loads, but they will not benefit from Declarative Shadow DOM on first paint. If your site relies heavily on BigPipe for lazy block rendering and those blocks contain web components, be aware of this limitation.

**Can I use this in production?**The binary is statically linked with no runtime dependencies. The protocol is simple (NUL-delimited pipes). The failure mode is graceful (returns original HTML). So... probably? But this is still early days. File issues, send PRs, report back.

**Why "Backlit"?**Because your Lit components are lit from behind -- server-rendered before the browser even sees them. Also because naming things is hard and this one was available.

Links
-----

[](#links)

- [lit-ssr-wasm](https://github.com/bennypowers/lit-ssr-wasm) -- the WASM module, Go library, and browser demo
- [Live demo](https://bennypowers.github.io/lit-ssr-wasm/compiled.html) -- runs the WASM module in your browser
- [Blog post](https://bennypowers.dev/posts/drupal-lit-ssr-wasm/) -- the full story
- [Previous approach (2024)](https://bennypowers.dev/posts/drupal-lit-ssr/) -- the Node.js sidecar version

License
-------

[](#license)

MIT

###  Health Score

34

—

LowBetter than 75% of packages

Maintenance78

Regular maintenance activity

Popularity9

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity35

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Total

2

Last Release

93d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/09287aab8926063e247e332a8c738a90d29152554537061a5df5cc195ac7b568?d=identicon)[bennypowers](/maintainers/bennypowers)

---

Top Contributors

[![bennypowers](https://avatars.githubusercontent.com/u/1466420?v=4)](https://github.com/bennypowers "bennypowers (7 commits)")

### Embed Badge

![Health badge](/badges/bennypowers-backlit/health.svg)

```
[![Health](https://phpackages.com/badges/bennypowers-backlit/health.svg)](https://phpackages.com/packages/bennypowers-backlit)
```

###  Alternatives

[blackbird/hyva-splide-js

An implementation of SplideJS library in Hyvä Theme for Magento 2

2018.3k](/packages/blackbird-hyva-splide-js)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
