matthecat/htmlcompressor-bundle
Symfony bundle that automatically minifies HTML responses, stripping unnecessary whitespace and comments to reduce page size and speed up delivery. Simple to install and configure, it helps improve front-end performance without changing templates or application code.
This is an archived Symfony 2-era bundle that hooks into kernel.response and minifies HTML/XML responses only when the response is cacheable. Treat it as a small, single-purpose listener rather than a framework you configure heavily.
Install and enable (Symfony 2.x style). The package is unmaintained and its Composer constraints target old Symfony/PHP versions; expect conflicts on modern stacks.
// app/AppKernel.php
public function registerBundles()
{
$bundles = [
// ...
new MatTheCat\HtmlCompressorBundle\MatTheCatHtmlCompressorBundle(),
];
}
Understand the default behavior before writing config. The bundle's main job is a response listener that checks $response->isCacheable(), inspects the Content-Type for HTML/XML, and rewrites the body with the htmlcompressor port. If you do nothing else, this is what runs.
Look first at these files in the repo:
EventListener/ or Listener/ — the kernel.response listener; this is the whole bundle in practice.DependencyInjection/Configuration.php — the real list of supported options (it is small).Resources/config/services.xml (or .yml) — service IDs and listener priority.First use case: a cached HTML page.
public function showAction($id)
{
$response = $this->render('post/show.html.twig', ['post' => $post]);
// Make the response cacheable so the bundle compresses it
$response->setPublic();
$response->setSharedMaxAge(600);
return $response;
}
Then verify with:
curl -sI https://example.test/post/1
curl -s https://example.test/post/1 | head -c 500
You should see the cache headers and a body with collapsed inter-tag whitespace/comments removed (depending on configuration).
The critical first mental model: this bundle does not minify every response — it minifies responses you have already marked as publicly cacheable.
Opt in via cache headers, not via bundle configuration. The normal workflow is to decide per controller which responses deserve compression by making them shared-cacheable:
$response->setPublic();
$response->setSharedMaxAge(3600);
// or
$response->headers->addCacheControlDirective('public');
$response->headers->addCacheControlDirective('max-age', 3600);
Private, no-store, non-200, and uncacheable responses pass through untouched. This is intentional: you avoid CPU work and corruption risk on user-specific pages.
Pair it with Symfony's HttpCache or a reverse proxy. The bundle rewrites the response before it is stored, so the cached copy is the compressed one. With Symfony's HttpCache:
// web/app.php
$kernel = new AppKernel('prod', false);
$kernel = new AppCache($kernel);
With Varnish/nginx proxy_cache, the same ordering applies: compression happens in the backend, caching in front.
Exclude specific responses without disabling the bundle. Since the gate is isCacheable(), the cleanest exclusion is to override cache headers at the end of the request:
$response->setPrivate();
$response->setMaxAge(0);
This is more reliable than relying on Content-Type sniffing or URL patterns.
Order your own kernel.response listeners before the compressor. If another listener rewrites HTML (injects a banner, rewrites links, adds a footer), it must run first. Check the bundle listener's priority and register yours higher:
$this->dispatcher->addListener('kernel.response', [$this, 'onKernelResponse'], 32);
The bundle's listener should be one of the last things touching the body.
Use the underlying compressor directly for non-response output if the bundle exposes it as a service (check debug:container for the service ID). Typical uses: minifying HTML before writing a static file, email templates, or generated XML sitemaps outside the HTTP cycle.
$compressor = $container->get('mat_the_cat_html_compressor.compressor');
$minified = $compressor->compress($html);
If it is not exposed, instantiate the compressor library directly; the listener contains no other magic.
Tune the compressor for your templates. The htmlcompressor family exposes switches such as comment removal, inter-tag space removal, multi-space removal, CSS/JS compression, quote removal, and preserve-pattern regexes. Enable the aggressive options incrementally and keep a preserve pattern list for things like:
// Protect pre/textarea and inline templates
$compressor->setPreservePatterns([
'/<\s*pre[^>]*>.*?<\s*\/\s*pre\s*>/is',
'/<\s*textarea[^>]*>.*?<\s*\/\s*textarea\s*>/is',
'/<\s*script[^>]*type=["\']text\/template["\'][^>]*>.*?<\s*\/\s*script\s*>/is',
]);
Test the listener boundary, not the compressor internals. In functional tests, assert that a cacheable HTML response is minified and a private response is byte-for-byte untouched. In the test environment, responses are usually not cacheable, so you may need to set cache headers explicitly in the test or disable the listener in test config if it interferes with assertions.
Configuration is set-and-forget. The bundle is small enough that the config tree is essentially "enable/disable plus compressor options." Clear the cache after changes:
app/console cache:clear --env=prod
because listener and compressor settings are compiled into the container.
isCacheable() === false. Symfony considers a response cacheable only for certain status codes and when Cache-Control says public, max-age, or s-maxage without private/no-store. Debug by dumping headers and checking the response before assuming the listener is broken.Content-Length is a real truncation risk. If the response already has a Content-Length header, rewriting the body can leave a larger value, causing clients or proxies to truncate. Remove it after compression or ensure your setup recalculates it:
$response->headers->remove('Content-Length');
304 flows this is usually harmless, but avoid re-compressing responses whose ETag is used as strong validation elsewhere.StreamedResponse, BinaryFileResponse, or empty bodies. getContent() can return false for streams; guard the listener or exclude these response types. Binary payloads with an HTML-ish Content-Type are a corruption waiting to happen.<script> or <style> can alter semantics (especially with automatic semicolon insertion or CSS descendant selectors). Disable JS/CSS compression first, then enable one at a time.<pre>, <textarea>, inline elements (<span> spacing), CDATA in XML, and old IE conditional comments. Use preserve patterns and test with real templates, not toy HTML.dev, the web debug toolbar injects markup and responses are typically non-cacheable, so compression usually won't run. Don't debug minification behavior in dev; use a prod-like environment.kernel.response listener replaces the content, the compression is wasted or the body is re-broken. Use debug:event-dispatcher (Symfony 2.6+) or inspect the service definition to confirm ordering.kernel.response fires for sub-requests too. If the listener does not check isMasterRequest(), fragments may be compressed individually and again inside the master response. Usually idempotent, but wasteful and occasionally destructive.voku/html-min, wyrihaximus/html-compress), a response listener you own, or compression at the CDN/proxy layer. If you inherit this bundle, isolate it behind your own listener/service so it can be swapped out.kernel.response listener service is the cleanest way to add exclusions, per-route rules, or logging; the bundle has little other surface area.How can I help you explore Laravel packages today?