22: Performance
A Phlo app that follows the conventions in this guide starts from a strong Lighthouse position, because the architecture already does most of what the audits measure: server-rendered HTML, one small deferred script, one stylesheet, no framework payload and no third-party requests. This chapter maps correct Phlo use to the web's performance best practices, and covers the part that remains yours: images, markup and measurement.

22.1: What the platform already does
Every view() response is complete server-rendered HTML. The first paint is real content, there is no hydration step and no client-side rendering to wait for, so First Contentful Paint and Largest Contentful Paint are governed by your server time and your images, not by a framework.
The assets are equally spare. The build compiles all frontend code into one app.js (the phlo.js runtime plus your compiled scripts, typically a few tens of kilobytes) and all styles into one app.css. view() includes the script with defer, so it never blocks rendering, and emits Link: rel=preload headers for the stylesheet, the scripts and the icon sprite, so the browser starts fetching them before it parses the HTML. Every asset URL carries a ?version cache-buster from %app->version, which makes long cache lifetimes safe: bump the version on release and every client re-fetches exactly once.
In front of that, FrankenPHP serves HTTP/2 and HTTP/3 with automatic HTTPS and compresses responses (zstd, brotli, gzip) when your vhost enables it. None of this needs configuration in the app.
22.2: Server time: worker mode
Time to First Byte is the one Lighthouse metric that lives entirely on your server. In production, run the release build in worker mode (thread in the entry): the app stays compiled and resident in memory, so a request executes your route without any bootstrap. Development mode (build: true) checks sources and may rebuild during a request; it is the right mode to develop in and the wrong mode to benchmark.
Beyond that, server time is ordinary backend discipline: keep queries indexed and few (chapter 8 covers ORM caching), and push slow work out of the request. For genuinely slow responses, chunk() streams output line by line, so the browser renders progressively instead of staring at a blank page. For recurring work, %app->tasks moves it out of requests entirely.
22.3: Images
Images decide most real-world LCP scores, and they stay your responsibility. The bundled img resource resizes, crops and converts with GD, so serve scaled files (a thumbnail where a thumbnail is shown) and prefer WebP output. In the markup, give every <img> a width and height (or CSS aspect-ratio) so the layout does not shift when it loads, use loading=lazy for images below the fold, and never lazy-load the LCP image itself.
Static files should ship long-lived cache headers from the webserver (Cache-Control: public, immutable with a long max-age); the ?version buster on generated assets makes that safe, and image URLs that change when content changes do the same for uploads.
22.4: Navigation: the SPA without the framework bill
With the DOM/link resource (or app.get() from your own script), an in-app navigation is one XHR returning apply() commands: the server sends only the DOM changes, the client applies them and path: updates the URL. There is no full reload, no re-download of CSS and JS, and Back and Forward restore snapshots instantly from history. View transitions animate the swap without costing a round trip.
This is where Phlo's zero-dependency stance pays directly in the Performance and Best Practices audits: there is no framework bundle to download, parse and execute, and no third-party origin to connect to. The blocking-time metrics (Total Blocking Time, interaction latency) stay low because the total JavaScript on the page is the runtime plus what you wrote, nothing more.
22.5: The other three audits
Accessibility is largely markup, and Phlo hands you the markup unwrapped: no generated wrappers, no synthetic buttons. Use the semantic element (<button>, <nav>, <main>), give images meaningful alt text, label form fields, and keep text contrast up in your theme. The lang attribute is set from %app->lang automatically.
Best Practices wants HTTPS, a sane console and no deprecated APIs. FrankenPHP's automatic HTTPS covers transport; the security resource's CSP modes (chapter 12) cover policy; and the engine's deprecation-free discipline keeps the console clean. Check the console on the release build once per release: a stray 404 on an asset costs points and is always a real bug.
SEO is chapter 17: the seo resource ships the sitemap, robots, canonical link and meta set from props you already define. A Phlo app with seo active and indexable set correctly scores this audit without further work.
22.6: Measuring honestly
Measure the release build on the production host, in a private window, against the public URL. Development mode skews every number: build: true may rebuild during the request, debug appends a debug payload to responses, and a dev vhost may lack the compression and cache headers of the production one. Lighthouse lives in Chrome DevTools; PageSpeed Insights runs the same audits from Google's infrastructure and adds field data when your site has traffic.
When a score drops, read the trace before changing code: the audit names the resource and the milliseconds. In a correctly configured Phlo app the usual suspects are, in order: an unscaled or non-lazy image, a slow query in the route, and a missing cache header on a static file. All three are visible in the waterfall, and none of them is the framework.