## NAME Mojolicious::Plugin::Buffer - Optimise, compress and validate Mojolicious responses ## VERSION Version 0.001.0 ## SYNOPSIS ### 1. The smallest set-up One line gives every page compression, an `ETag`, a `Last-Modified` header and `304 Not Modified` answers: ```perl use Mojolicious::Lite; plugin 'Buffer'; get '/' => sub { my $c = shift; $c->render(template => 'index'); }; app->start; ``` ### 2. Smaller HTML Also remove extra white space from HTML pages. Install [HTML::Packer](https://metacpan.org/pod/HTML%3A%3APacker) to remove comments and shrink embedded CSS as well: ```perl plugin 'Buffer' => {optimise_content => 1}; ``` ### 3. A server-side cache for slow pages Keep finished pages in a cache, so a slow action runs once and later visitors get the stored copy. Call the helper _first_ in the action, but _after_ any check that decides who may see the page: ```perl use CHI; plugin 'Buffer' => {cache => CHI->new(driver => 'File', root_dir => '/var/cache/myapp')}; get '/report' => sub { my $c = shift; # Access checks come first: a cached page is sent without them return $c->reply->not_found unless $c->session('user'); return if $c->buffer_is_cached; # sent from the cache: stop here $c->render(text => slow_report()); # only runs on a miss }; ``` ### 4. Several workers on one machine With [hypnotoad](https://metacpan.org/pod/hypnotoad) there are several worker processes. Share the cache between them, and give them a lock directory, so only one worker builds a missing page: ```perl plugin 'Buffer' => { cache => CHI->new(driver => 'File', root_dir => '/var/cache/myapp'), cache_lock_dir => '/var/lib/myapp/buffer-locks', }; ``` For workers on several machines, see ["Multiple hosts"](#multiple-hosts). ### 5. One response is different Put a `buffer` value in the stash. `0` leaves the response alone; a hash changes some options for this response only: ```perl get '/download' => sub { my $c = shift; $c->stash(buffer => 0); # already compressed: do not touch it $c->render(data => $zip_file, format => 'zip'); }; get '/raw' => sub { my $c = shift; $c->stash(buffer => {optimise_content => 0}); $c->render(template => 'raw'); }; ``` ### 6. A whole section is different Some options can be set once for a group of routes: ```perl my $docs = app->routes->under('/docs')->to(optimise_content => 1); $docs->get('/intro' => sub { shift->render(template => 'docs/intro') }); ``` ### 7. Pages in several languages Add `Accept-Language` to the cache key, so each language is stored separately: ```perl plugin 'Buffer' => { cache => $cache, vary_by => ['cookie', 'accept-language'], }; ``` ### 8. Translate the "busy" message While one request builds a page, a request that waits too long gets `503 Service Unavailable` with a short text. Change the text with `messages`: ```perl plugin 'Buffer' => { cache => $cache, messages => {retry_later => "Bitte versuchen Sie es gleich noch einmal.\n"}, }; ``` ### 9. A full application ```perl package MyApp; use Mojo::Base 'Mojolicious'; use CHI; sub startup { my $self = shift; $self->plugin('Buffer', { optimise_content => 1, cache => CHI->new(driver => 'File', root_dir => '/var/cache/myapp'), cache_lock_dir => '/var/lib/myapp/buffer-locks', cache_duration => '10 minutes', vary_by => [], # these pages are the same for everyone }); $self->routes->get('/')->to('pages#home'); } 1; ``` ## DESCRIPTION `Mojolicious::Plugin::Buffer` makes the pages of a [Mojolicious](https://metacpan.org/pod/Mojolicious) application smaller and faster to deliver, without changes to your actions. It is the Mojolicious version of [CGI::Buffer](https://metacpan.org/pod/CGI%3A%3ABuffer) and [FCGI::Buffer](https://metacpan.org/pod/FCGI%3A%3ABuffer). ### What it does After your action has made a page, and before the page is sent, the plugin can: - **make HTML smaller** by removing extra white space and, with [HTML::Packer](https://metacpan.org/pod/HTML%3A%3APacker), comments and extra characters in embedded CSS and JavaScript. It also shortens links to your own site; - **compress the page** with Brotli, Zstandard or gzip, whichever the browser accepts and the server has installed; - **add headers that let browsers reuse a copy they already have** (`ETag` and `Last-Modified`), and answer "you already have this" with a short `304 Not Modified` reply instead of the whole page; - **set the correct `Content-Length`** after all of these changes; - **keep finished pages in a server-side cache**, if you ask it to, so that a slow action does not have to run again for the next visitor (see ["buffer\_is\_cached"](#buffer_is_cached)). ### How it works, in plain words - 1. When the application starts, ["register"](#register) checks your options and installs a _hook_: a small function that Mojolicious calls for every response, after the action has finished (`after_dispatch`). - 2. A request arrives and your action runs. If the action calls ["buffer\_is\_cached"](#buffer_is_cached) and a fresh copy of the page is in the cache, the copy is sent and the action stops early. - 3. Otherwise the action makes the page as usual. - 4. The hook looks at the finished response. It leaves alone anything it should not touch: streamed responses, static files, empty bodies, statuses that have no body (1xx, 204, 206, 304), responses that are already compressed, and media types listed in ["excluded\_types"](#excluded_types). - 5. For an HTML page it removes white space, if ["optimise\_content"](#optimise_content) is on. - 6. It compresses the page for this browser, if the media type is in ["compressible\_types"](#compressible_types). - 7. It stores the _uncompressed_ page in the cache, if the action called ["buffer\_is\_cached"](#buffer_is_cached) and the page may be stored. - 8. It adds the `ETag` and `Last-Modified` headers. If the browser already has this version, it sends `304 Not Modified` with no body and stops. - 9. Otherwise it puts the compressed (or plain) page into the response, sets `Content-Length`, and the page is sent. Error pages such as `404` go through the same steps, but only `200` responses get an `ETag` or are stored in the cache. The plugin works through a hook, so it also sees responses built with `$c->res->body` rather than `$c->render`. Unlike the CGI modules it needs no source filter and no `END` block. ### Words used in this document - cache A store that keeps finished pages for a while, for example [CHI](https://metacpan.org/pod/CHI). The plugin needs an object with `get`, `set` and `remove` methods. - cache key The name under which a page is kept in the cache. By default it is made from the host, path, query string and the request headers listed in ["vary\_by"](#vary_by). - ETag A short fingerprint of the page, sent in a header. A browser that has a copy sends the fingerprint back; if it still matches, the server answers `304`. - 304 Not Modified A reply with no body that means "the copy you have is still correct". - content coding The compression method of a response, such as `gzip` or `br` (Brotli). The browser lists what it accepts in `Accept-Encoding`. - worker One of several copies of the application that a server such as [hypnotoad](https://metacpan.org/pod/hypnotoad) runs at the same time. Workers do not share memory. - single-flight When many requests ask for the same missing page at once, only one of them builds it; the others wait and then get the result. - stash Mojolicious's per-request hash of values, `$c->stash`. The plugin reads its per-response options from `$c->stash('buffer')`. ## OPTIONS All options can be given to `plugin()` and, except where noted, overridden per request through the `buffer` stash value. Options given to `plugin()` are validated when the plugin is registered; values in the `buffer` stash are not, and an unusable one falls back to its default where that is safe. A few options describe a section of a site rather than one page and can be declared once for it; see ["Declaring options for a group of pages"](#declaring-options-for-a-group-of-pages). ### Declaring options for a group of pages ```perl # one route $r->get('/about' => {optimise_content => 1} => sub {...}); # everything beneath an under route my $docs = $r->under('/docs')->to(optimise_content => 1); $docs->get('/intro' => sub {...}); # a controller, and through a base class its whole namespace package MyApp::Controller::Docs; use Mojo::Base 'Mojolicious::Controller'; has buffer_optimise_content => 1; ``` A route default is an ordinary stash value, so `$app->defaults(...)` and a `before_action` that sets one work as well. The controller form is a method named `buffer_` followed by the option name, and is only consulted when the stash says nothing: `has` is the declarative way to write it, and a plain `sub buffer_optimise_content { 1 }` covers a value that depends on the request. A controller method that dies is logged at debug level and ignored, so the plugin option stays in force. The most specific declaration wins: - 1. the `buffer` stash, for a single response - 2. a route default, which includes one an `under` route sets - 3. the controller declaration - 4. the plugin option A route default of `0` therefore opts one page back out of a group that has the option on, and the `buffer` stash overrides both. Only the options that describe the content of a section are declarable: - ["compress\_content"](#compress_content) - a section serving files that are already compressed - ["optimise\_content"](#optimise_content) - a static section that is always minified - ["rewrite\_links"](#rewrite_links) - a section whose HTML is used elsewhere, so its absolute links must survive - ["trust\_last\_modified"](#trust_last_modified) - a section that renders templates and nothing else Everything else, including ["generate\_etag"](#generate_etag) and ["generate\_304"](#generate_304), stays a per-response or per-application decision; those sections say why. Any option can still be set for one response through the `buffer` stash. `examples/grouped-sections` is a runnable site with one section declared on an `under` route and one declared in a controller. ### compress\_content Compress eligible responses, defaults to 1. Set to 0 to only optimise (and remember that the plugin turns off ["compress" in Mojolicious::Renderer](https://metacpan.org/pod/Mojolicious%3A%3ARenderer#compress), so a response will then not be compressed at all). The encodings offered to clients are controlled by ["encodings"](#encodings). It can be declared for a group of pages (see ["Declaring options for a group of pages"](#declaring-options-for-a-group-of-pages)), which is the point of a section that serves files it has already compressed. ### optimise\_content How hard to optimise HTML, defaults to 0 (off). Registration rejects any value other than 0, 1 or 2: - 0 - leave the HTML alone - 1 - remove superfluous white space, rewrite same-site links and, when [HTML::Packer](https://metacpan.org/pod/HTML%3A%3APacker) is installed, remove comments and minify embedded CSS - 2 - as 1, plus aggressive JavaScript minification when [HTML::Packer](https://metacpan.org/pod/HTML%3A%3APacker) is installed, which can break scripts that rely on the `` hiding technique Optimisation only applies to `text/html` and `application/xhtml+xml` responses. The content of `
`, `