NAME
Catalyst::Seal - freeze a Catalyst application at setup and make it 4x faster
VERSION
Version 0.01
seal
Catalyst::Seal->seal('MyApp');
Runs every registered step against the application class. Called for you from setup_finalize; call it directly only if you are sealing an application that did not use Catalyst::Seal.
Each step runs in its own eval. A step that dies is a bug in this distribution, so it always warns rather than disappearing into the hook, and the remaining steps still run.
SYNOPSIS
package MyApp;
use Catalyst::Seal;
use Catalyst qw/ ConfigLoader Static::Simple /;
__PACKAGE__->setup();
DESCRIPTION
After setup_finalize a Catalyst application is frozen: the class tree, the configuration, the action table and the attribute layouts all stop changing. Catalyst goes on re-deriving those facts from the metaobject protocol on every request. Catalyst::Seal makes one pass at the end of setup() and compiles the frozen facts into constant form.
No application code changes. Place the use line above use Catalyst and nothing else.
What this module does
Makes the application class and its components immutable, which Catalyst does for controllers but not for the application class, even though the application class is also the per-request context class.
Replaces the two Try::Tiny blocks on the request path with plain
eval.Replaces every
mk_classdataaccessor with an XS constant. The stock one callsMoose::Util::find_metaon every read to answer a question that stopped changing atsetup_finalize: 84 of those calls per request on a bare application. A write unseals the accessor and puts the original back.Takes
find_metaout of theCatalyst::Component::configread path.Installs the composed body of every method carrying a
before,afteroraroundmodifier directly, in place of the trampoline Class::MOP::Method::Wrapped installs, which callsset_subnameon every invocation to attach a name that has not changed since the modifier was applied. Ten of those are on the request path. A modifier added after the seal puts the trampoline back.Short-circuits the Catalyst::Response guard that warns about setting a header after the headers were finalised, which on a read evaluates three accessors to reach a condition that already could not be true.
Seals
$c->configas a constant. Catalyst croaks on any write to it once setup has finished, so after setup it cannot change, and the wholearound,find_meta,get_or_add_package_symbolchain behind it is dead weight on every one of the nine reads a request makes.Replaces the attribute readers on the context, request and response classes with XS. Only readers, only on immutable classes, and only where the reader is Moose's own. A lazy attribute whose slot is not built yet delegates to that reader, so the builders stay Moose's and a
predicatekeeps telling the truth.Aliases
req,resandcompstraight to the methods they delegate to, removing a whole frame from each of 25 calls a request.Memoises the two action lookups the forward chain repeats on every request. One action costs five forwards, each of which turns a string like
'/foo/_BEGIN'into an action object by walking a table that stopped changing atsetup_finalize. The chain itself is not flattened: the private steps go on$c->stack, and$c->depthis what gates thedetachandgorethrows, so a flat chain would have to reimplementexecuteto keep them honest.Memoises the encoding decision.
finalize_encodingspends 49 us per request deciding that atext/plainbody does not need encoding, by callingcontent_typefour times andcontent_type_charsetthree times to re-parse one header string. The answer is a pure function of the raw content type, the content encoding, the encodable-type pattern and the application encoding.Replaces the Try::Tiny in Moose's inlined destructor for Catalyst::Response with an
eval. Two closures were being built on every response destroyed, to call a four lineDEMOLISH.Replaces the Try::Tiny in
_handle_param_unicode_decodingwith aneval. Every query parameter, body parameter and path argument is decoded through it, name and value separately, so a four parameter query string builds sixteen closures to decode eight strings. Worth 15 us on such a request, which is more than the parsing it surrounds.Builds the HTTP::Headers hash in
prepare_headersdirectly rather than through oneheadercall per environment key. The spelling of each field is asked of HTTP::Headers once, the first time a request carries it, and remembered, so nothing here has to know which headers HTTP::Headers considers standard.Skips
URI::canonicalinprepare_pathwhen the URI just built is already canonical, which is one regex to decide and three authority parses to ask. Checked against URI itself at seal time, positive and negative, rather than against its source.Stops a controller's
BUILDfiring two lazy builders on the per-request context object. The application class inherits Catalyst::Controller, soBUILDALLruns itsBUILDon every context object, to compute values derived entirely from class data.Defers building the Catalyst::Stats object when stats are disabled. Its
treeattribute is required and not lazy, so stock builds a Tree::Simple and reads the clock on every request for an object nothing then reads. With stats enabled nothing changes, because that timestamp is the request start.
On a bare application, 267.4 to 78.0 us per request, and on Hyperman under wrk 3,740 to 20,340 requests per second.
cat-hello 6586 req/s
cat-seal 20461 req/s
Environment
CATALYST_SEAL=0-
Hard kill switch. Nothing is sealed and the application is stock Catalyst.
CATALYST_SEAL_DEBUG=1-
Report what was sealed, and what was skipped and why.
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)