Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Html Compress Laravel Package

wyrihaximus/html-compress

A lightweight PHP package that compresses HTML output by removing unnecessary whitespace, comments, and redundant markup. It helps reduce response size, improve page load times, and save bandwidth with minimal integration effort.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and verify the factory works:

composer require wyrihaximus/html-compress

The package has no Laravel service provider and no auto-discovery — it is a plain PHP library. You get two entry points from WyriHaximus\HtmlCompress\Factory:

use WyriHaximus\HtmlCompress\Factory;

$compressor = Factory::construct();      // DOM-based, thorough, slower
// $compressor = Factory::constructFast(); // regex/string based, much faster, less safe

$html = $compressor->compress($html);

Both return an implementation of WyriHaximus\HtmlCompress\Compressor (compress(string $html): string). That interface is the only thing you should depend on in your app code.

Where to look first: the Factory class (to see which compressor you get), the Compressor interface, and the package's test suite (to understand what the DOM-based pass actually rewrites — comments, whitespace, quotes, optional tags).

First use case: compress the final HTML response once, globally, via middleware. Register a singleton so you build the compressor once per request lifecycle:

// app/Providers/AppServiceProvider.php
use WyriHaximus\HtmlCompress\Compressor;
use WyriHaximus\HtmlCompress\Factory;

public function register(): void
{
    $this->app->singleton(Compressor::class, fn () => config('html-compress.fast')
        ? Factory::constructFast()
        : Factory::construct());
}
// config/html-compress.php
return [
    'enabled' => env('HTML_COMPRESS', ! app()->isLocal()),
    'fast'    => env('HTML_COMPRESS_FAST', false),
];

Then a single middleware appended to the web group:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use WyriHaximus\HtmlCompress\Compressor;

class CompressHtml
{
    public function __construct(private readonly Compressor $compressor) {}

    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        if (! $this->shouldCompress($request, $response)) {
            return $response;
        }

        $response->setContent($this->compressor->compress($response->getContent()));
        $response->headers->remove('Content-Length');

        return $response;
    }

    private function shouldCompress(Request $request, Response $response): bool
    {
        if (! config('html-compress.enabled')) return false;
        if ($request->headers->has('X-No-Compress')) return false;
        if ($response instanceof \Symfony\Component\HttpFoundation\StreamedResponse) return false;
        if ($response instanceof \Symfony\Component\HttpFoundation\BinaryFileResponse) return false;
        if ($response->isRedirection() || ! $response->isSuccessful()) return false;
        if (! str_contains(strtolower($response->headers->get('Content-Type', '')), 'text/html')) return false;

        return true;
    }
}

Register it last in the web group (Laravel 11+ bootstrap/app.php → $middleware->web(append: [...])) so it sees the fully rendered response, and keep an escape hatch (X-No-Compress) for debugging.

Implementation Patterns

Middleware vs. response macro vs. Blade. Middleware is the right default: one place, all HTML responses, no view changes. Use a response macro only if you need per-route opt-in:

use Illuminate\Http\Response;

Response::macro('compressed', function (string $content, int $status = 200) {
    return response(app(\WyriHaximus\HtmlCompress\Compressor::class)->compress($content), $status)
        ->header('Content-Type', 'text/html; charset=UTF-8');
});

Avoid a Blade directive/ob_start wrapper — it interacts badly with @extends, cached views, and streamed output.

Compress once, cache the result. Compression costs CPU per request. For stable pages, compress at the cache layer instead of every response:

$html = Cache::remember("page:{$slug}", now()->addHour(), function () use ($slug) {
    return app(Compressor::class)->compress(view('pages.show', ['page' => Page::find($slug)])->render());
});

return response($html)->header('Content-Type', 'text/html; charset=UTF-8');

This pairs well with full-page response caching and means the middleware becomes a no-op for hot paths (the content is already minified; a second pass is wasted work — see Gotchas).

Pick the compressor per route type. constructFast() is fine for large marketing pages with well-formed HTML; use the DOM-based construct() where markup correctness matters (forms, SVG, email templates rendered to HTML). Expose it via config rather than branching in controllers.

Keep it out of JSON/API and framework tooling. The Content-Type guard handles most of it, but also skip when app()->runningUnitTests() is irrelevant — tests should exercise the real path — and skip for Telescope/Debugbar routes, which inject their own markup.

Testing. Compression changes whitespace and attribute quoting, so assert on meaning, not bytes:

it('serves compressed html', function () {
    $this->get('/pricing')
        ->assertOk()
        ->assertHeader('Content-Type', 'text/html; charset=UTF-8')
        ->assertSee('Pricing', false)
        ->assertDontSee('<!-- raw marker -->');
});

Add one test that hits a page containing <pre>, <textarea>, inline <script>, and Alpine/Livewire attributes — that's where regressions hide.

Decorator pattern for extension. Since everything goes through the Compressor interface, wrap it to add logging, timing, or a no-op in debug:

final class ConditionalCompressor implements Compressor
{
    public function __construct(private Compressor $inner) {}

    public function compress(string $html): string
    {
        return app()->hasDebugModeEnabled() ? $html : $this->inner->compress($html);
    }
}

Bind that in the container and the rest of your code never changes.

Gotchas and Tips

  • Whitespace is semantic. The DOM-based compressor will collapse whitespace that matters inside <pre>, <textarea>, and between inline elements (<span>a</span> <span>b</span>). Always render a smoke-test page and diff the output visually before enabling globally.
  • Inline JS/CSS is the riskiest part. Anything that depends on comments (//, <!--), template literals, or </script> inside strings can be mangled. If your pages carry inline <script> or CSP hashes/nonces, either disable JS/CSS compression options or use constructFast() and verify. CSP hashes will break the moment whitespace changes — prefer nonces.
  • Framework attribute expressions. Alpine (x-data, @click), Livewire (wire:model), and Vue interpolations often contain quotes, > and <. Test these pages specifically; DOM parsers are the usual culprit when an attribute "mysteriously" becomes empty.
  • Remove Content-Length after rewriting. Symfony doesn't always recompute it, and a stale value truncates the body behind nginx/Apache. Also skip StreamedResponse and BinaryFileResponse — getContent() is false for those and you'll throw.
  • Don't double-compress. If a fragment was cached post-compression, running middleware over it again wastes CPU for zero gain. Either cache raw and compress in middleware, or cache compressed and skip the middleware for that route — pick one.
  • Ordering matters. Middleware that appends to the body after yours (Debugbar, Telescope injection, some analytics packages) bypasses compression. Put CompressHtml last in the stack, and accept that Debugbar output won't be minified.
  • This is not a substitute for gzip/brotli. HTML minification saves maybe 5–15% before transfer encoding; enable gzip/brotli in nginx/Caddy and keep HTTP caching headers. Compression is a CPU-for-bytes trade, and brotli usually wins bigger.
  • HTML comments disappear. Don't use comments as server-side markers, conditional-comment hacks, or hydration anchors. If a downstream tool parses them, it will break.
  • Low adoption, small surface. 79 stars and no dependents means you should pin the version in composer.json, read the changelog on upgrades, and keep the Compressor interface as your only coupling point in app code — swapping in voku/html-min or a custom regex pass later should be a one-line container rebind.
  • Octane-safe. The compressor is stateless, so a singleton is fine; just don't stash per-request HTML on the instance. If you build a custom decorator, keep it stateless too.
  • Debugging tip: temporarily log strlen($before) vs strlen($after) per request, and expose ?no-compress=1 (checked in middleware) so you can diff raw vs. compressed output in the browser when a layout breaks.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource