Hyvä Response Minification and Merging
The hyva-themes/magento2-minification module provides HTML, inline CSS, and inline JavaScript
response minification for Hyvä themes via a Node.js daemon.
It minifies full-page HTML responses before they leave PHP (and before Varnish caches them), using @swc/html — a native Rust-based minifier that handles HTML, JavaScript, and CSS in a single pass with Alpine.js-safe settings.
Performance Trade-off: Slower Uncached Responses
Important: Enabling minification increases response time for uncached pages. Every cache-miss response is sent to the minifier daemon before being returned to the browser, which adds roughly 70–120 ms of latency per uncached request (see the Daemon Throughput benchmark below).
Cache hits — both Varnish full-page cache and the Magento block_html cache — are not affected; they serve the already-minified response directly.
Enabling only JS merging does not cause this performance impact.
APDEX Impact
Because uncached responses are slower, your APDEX score will drop slightly after enabling minification. The size of the drop depends on your cache hit rate and your current APDEX target (T) value — the lower the cache hit rate, the larger the impact.
Recommendation: after enabling minification, raise your APDEX target time by approximately the daemon's typical added latency (around 100 ms) so the score reflects the new baseline response time rather than flagging every uncached page as "tolerating" or "frustrating". For example, if your current APDEX T is 500 ms, consider raising it to 600 ms.
The trade-off is intentional: minification shifts cost from many cached responses (smaller payloads, faster downloads, less bandwidth, lower CSP header size) to a one-time cost on cache misses. On sites with a healthy Varnish hit rate, the net user-perceived performance improves despite the slower cache-miss path.
Strict CSP Compatibility
The hyva-themes/magento2-minification module is compatible with both hyva-themes/magento2-default-theme
and hyva-themes/magento2-default-theme-csp (strict Content-Security-Policy). SHA-256 hashes for inline
scripts are automatically computed from the minified content before CSP headers are written, so browsers
receive matching hashes for the minified scripts.
This works transparently for all page types, including pages with Varnish ESI blocks and blocks served from the block_html cache.
Requirements
- Magento 2.4.x with Hyvä Theme ^1.3.11
- Node.js >= 20
- PHP 8.1+
Installation
composer require hyva-themes/magento2-minification
bin/magento module:enable Hyva_Minification
bin/magento setup:upgrade
Starting the Daemon
The daemon listens on 127.0.0.1:3100 by default.
Security: The daemon has no authentication. Keep it bound to
127.0.0.1(the default) and do not setMINIFIER_HOSTto0.0.0.0or a public interface. In containerized environments, ensure the daemon port is not exposed outside the pod or host network.
Environment Variables
| Variable | Default | Description |
|---|---|---|
MINIFIER_PORT |
3100 |
TCP port |
MINIFIER_HOST |
127.0.0.1 |
Bind address |
MINIFIER_SOCKET |
(empty) | Unix socket path (overrides TCP when set) |
MALLOC_ARENA_MAX |
2 (set by Magento-managed start) |
Caps glibc malloc arenas — see below |
Memory: MALLOC_ARENA_MAX
When the daemon is started by Magento (cron, on-demand, or admin button), it is launched
with MALLOC_ARENA_MAX=2 in its environment. This caps glibc's per-thread allocator pools
at 2 instead of the default 8 × CPU cores.
The HTML/JS minifier produces many small short-lived allocations (AST nodes, attribute strings, mangler symbol tables). glibc rarely returns freed memory to the OS — it keeps it in per-arena free-lists. On multi-core hosts this can push the daemon's RSS into the tens of GB even though the actual working set is small. Capping arenas bounds the fragmentation overhead, typically reducing long-term RSS by an order of magnitude.
Trade-off: threads now share arenas, so malloc/free can contend on an arena lock. The daemon is single-threaded JS with a small libuv worker pool, so contention is negligible in practice.
The variable is harmless on platforms that don't use glibc (musl/Alpine, macOS, BSD) —
those allocators ignore it. If you run the daemon under a system service manager
(systemd, supervisor, etc.), add MALLOC_ARENA_MAX=2 to the unit's environment as shown
in the systemd example above.
Unix Socket Example
Running in Production
Use a process manager to keep the daemon alive. Example systemd unit:
[Unit]
Description=Hyvä Minifier Daemon
After=network.target
[Service]
Type=simple
Environment=MALLOC_ARENA_MAX=2
WorkingDirectory=/var/www/html/vendor/hyva-themes/magento2-minification/node
ExecStart=/usr/bin/node daemon.mjs
Restart=always
User=www-data
[Install]
WantedBy=multi-user.target
Daemon Management
On environments where a system service is not available (e.g. Adobe Commerce Cloud), the daemon can be managed automatically. Navigate to Stores > Configuration > Hyvä Themes > System > Response Minification > Daemon Management and select one of:
| Mode | Description |
|---|---|
| Off (default) | The daemon must be started manually or via a process manager (systemd, supervisor, etc.) |
| Cron | A watchdog cron job runs every minute — if the daemon is not responding, it starts a new instance |
| Start on-demand | The daemon is started automatically on the first request that needs it. A file lock prevents concurrent requests from spawning multiple processes, and a failure counter stops retrying after 30 consecutive failures (auto-resets after 1 hour or when the daemon becomes healthy) |
Or using the CLI:
bin/magento config:set hyva_minification/general/daemon_management cron
bin/magento config:set hyva_minification/general/daemon_management on_demand
bin/magento config:set hyva_minification/general/daemon_management off
Legacy env.php setting: The
hyva_minification/cron_daemon_manager_enabledsetting inapp/etc/env.phpis still supported. When present and truthy, it takes precedence over the admin panel setting and locks the dropdown to Cron. Remove the env.php setting to manage this option in the admin panel instead.
When minification is enabled, the admin panel shows a daemon status panel with:
- Whether the daemon is running
- Current daemon management mode
- PID and last start time (when available)
- Daemon log file size
- A Start, Stop, or Restart button depending on the current state
To restart the daemon without server access (e.g. after a deploy), click the restart button in the admin panel, or create a restart signal file manually:
The daemon checks for this file every 5 seconds. When found, it shuts down cleanly. If daemon management is set to Cron or Start on-demand, a new instance is started automatically.
File permissions (Cron mode): The cron daemon manager requires that both the web server user and the cron process can read and write the same files in
var/(PID file, log, restart flag). This is the case on Adobe Commerce Cloud out of the box. For on-premise deployments, ensure both processes run as the same OS user, or belong to a shared group with a consistent umask so that files created by one process remain writable by the other.See the Adobe Commerce documentation for details: - File ownership and permissions (overview) - Configure file ownership and permissions
Configuration
Daemon Connection (env.php)
The daemon address is configured in app/etc/env.php (not in the admin panel) so that only
users with server access can change where the PHP process connects.
The defaults work out of the box for a standard single-host setup (tcp://127.0.0.1:3100).
Override them when the daemon runs on a different host or uses a Unix socket.
Using [n98-magerun](https://github.com/netz98/n98-magerun2):
# TCP (default) — set host and port
n98-magerun config:env:set hyva_minification/transport tcp
n98-magerun config:env:set hyva_minification/tcp_host 127.0.0.1
n98-magerun config:env:set hyva_minification/tcp_port 3100
# Unix socket — set transport and socket path
n98-magerun config:env:set hyva_minification/transport unix_socket
n98-magerun config:env:set hyva_minification/socket_path /tmp/hyva-minifier.sock
This produces the following structure in app/etc/env.php:
'hyva_minification' => [
'transport' => 'tcp',
'tcp_host' => '127.0.0.1',
'tcp_port' => '3100',
// 'socket_path' => '/tmp/hyva-minifier.sock',
],
| Key | Default | Description |
|---|---|---|
transport |
tcp |
tcp or unix_socket |
tcp_host |
127.0.0.1 |
Daemon TCP host |
tcp_port |
3100 |
Daemon TCP port |
socket_path |
/tmp/hyva-minifier.sock |
Unix socket path (used when transport is unix_socket) |
cron_daemon_manager_enabled |
false |
Legacy. When true, locks daemon management to Cron (see above) |
Admin Panel
Navigate to Stores > Configuration > Hyvä Themes > System > Response Minification to configure:
- Enable — master toggle (store-scoped)
- Daemon Management — Off, Cron, or Start on-demand (global scope; see above)
- Merge Inline JavaScript — merge all inline scripts into a single
<script>tag (see below) - Connection Timeout — how long to wait for a connection before falling back (default: 100ms)
- Read Timeout — how long to wait for the minified response (default: 2500ms)
- Shell Fallback (Developer Mode) — fall back to shelling out to
nodewhen the daemon is unreachable (see below)
Merge Inline JavaScript
When enabled, all inline <script> tags are merged into a single <script> tag placed just
before </body>. This reduces the number of SHA-256 hashes in the Content-Security-Policy
header from one per inline script to just one for the merged result.
This is especially useful with hyva-themes/magento2-default-theme-csp (strict CSP), where a
typical page may contain 20+ inline scripts, each requiring its own hash in the CSP header.
Merging them into one script dramatically reduces header size, which benefits mobile
performance and avoids header-size limits in some proxies and firewalls.
Key details:
- Cacheable pages only — scripts are only merged on pages that are cacheable in the full page cache (FPC). Uncacheable pages (e.g. checkout, customer account) are left as-is, because they don't shas to authorize inline scripts but only a single nonce.
- Works independently of minification — the merge can be enabled without the Node.js daemon; it runs entirely in PHP
- Only merges JavaScript — scripts with
type="text/javascript"or notypeattribute are merged. Non-JS types (module,application/json,speculationrules, etc.) are left untouched - Varnish ESI compatible — scripts inside ESI blocks are captured and included in the merge
- Disabled by default — enable it in the admin panel or via CLI:
Behaviour
- Only minifies
text/htmlfrontend responses on Hyvä-themed storefronts - Admin panel and Luman theme responses are never minified
- If the daemon is unreachable or returns an error, the original unminified response is served and a warning is logged
- If the minified output is not smaller than the original, the original is served
- Varnish caches the minified response on cache miss; subsequent cache hits serve it directly
Developer Mode Features
In developer mode:
- An
X-Hyva-Minified: 1response header is added when minification was applied - Append
?hyva_minification=falseto any URL to bypass minification for that request
Shell Fallback
In developer mode, you can enable a shell fallback that minifies responses by shelling out to
node directly, without requiring the daemon to be running. This is slower than the daemon
(each request spawns a new Node process) but convenient for quick testing of whether a module
works with minification or merging.
Enable it in the admin panel under Stores > Configuration > Hyvä Themes > System > Response Minification > Shell Fallback (Developer Mode), or via CLI:
The fallback only activates when the daemon is unreachable and Magento is in developer mode. If the daemon is running, it is always preferred.
Custom Node Binary Path
If node is not on the default PATH (e.g. nvm-managed Node, or a non-standard container
layout), configure the path in app/etc/env.php.
The recommended location is the shared Hyvä key, so all Hyvä packages can use it:
For backward compatibility, the minification-specific key is also supported and takes precedence:
Lookup order: hyva_minification/node_binary → hyva/node_binary → node (from $PATH).
Benchmark: HTML Size Reduction
Measured on Magento 2.4.8-p3 with Hyvä Theme and sample data:
| Page | Minified | Unminified | Savings | % |
|---|---|---|---|---|
| Homepage | 334,419 | 538,823 | 204,404 | 37.9% |
| PLP (Category) | 410,999 | 758,874 | 347,875 | 45.8% |
| PDP (Configurable) | 302,750 | 519,881 | 217,131 | 41.8% |
| Login | 158,395 | 264,022 | 105,627 | 40.0% |
| Registration | 187,997 | 322,330 | 134,333 | 41.7% |
| Customer Dashboard | 198,347 | 349,158 | 150,811 | 43.2% |
| Order History | 194,409 | 342,948 | 148,539 | 43.3% |
| Wishlist | 175,188 | 294,121 | 118,933 | 40.4% |
| Cart | 260,330 | 465,930 | 205,600 | 44.1% |
| Checkout | 294,501 | 590,686 | 296,185 | 50.1% |
| Min | Max | Average | Median | |
|---|---|---|---|---|
| Savings | 37.9% | 50.1% | 42.8% | 42.5% |
Benchmark: Daemon Throughput
Measured with 20 concurrent requests, ~500 KB HTML payload, 60-second run:
| Metric | Value |
|---|---|
| Requests/sec | 167.28 |
| Avg latency | 120 ms |
| P50 latency | 120 ms |
| P95 latency | 124 ms |
| P99 latency | 128 ms |
| Errors | 0 |
| Input size | 505 KB |
| Output size | 383 KB |
| Size savings | 24.1% |
The daemon is single-threaded (Node.js event loop). Minification is CPU-bound, so requests
are serialized under concurrency. The native Rust-based @swc/html minifier processes each
request in ~70–120 ms. For production workloads behind Varnish, the daemon only processes
cache misses, so throughput is rarely a bottleneck.
This added per-request latency is the source of the slower uncached-response time and APDEX drop noted in Performance Trade-off: Slower Uncached Responses.
Verifying It Works
# Health check
curl http://127.0.0.1:3100/health
# Test minification directly
curl -s -X POST -H "Content-Type: text/html" \
-d '<html><body> <p>hello</p> </body></html>' \
http://127.0.0.1:3100/
License
This package is licensed under the Open Software License (OSL 3.0).
- Copyright: Copyright © 2020-present Hyvä Themes. All rights reserved.
- License Text (OSL 3.0): The full text of the OSL 3.0 license can be found in the
LICENSE.txtfile within this package, and is also available online at http://opensource.org/licenses/osl-3.0.php.