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.

A 100% Lighthouse score on all four audits for tour.phlo.tech, an unmodified Phlo app

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.

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.