A freelancer can lose an afternoon tuning a web app that still feels slow on a client’s phone. I would start with one representative page, a local test, and a small record of changes—not a monitoring subscription or a site-wide rewrite. That narrow approach is debatable, but it gives every hour of optimization a visible result and costs nothing beyond the tools already used to ship the app.
One representative page gives a better starting point than a site-wide score
Pick the page that matters to the client and has enough content to expose a real problem: perhaps a portfolio project with a large hero image, or the first screen of a booking form. Write down its URL, the action a visitor needs to take, and the device you expect them to use. Do not begin with the homepage merely because it is the homepage; a fast landing page says little about a slow form that clients actually need people to complete.
Open that page in Chrome DevTools, select the Network panel, disable the cache, and reload it once. Keep the waterfall. Look for the request that delays the main visible content, not the request with the largest file size in isolation. A 60 KB stylesheet that blocks rendering can matter more to the first view than a 400 KB image far below the fold, because the browser needs the stylesheet before it can paint that view. Note the page’s largest visible element and any layout movement you can see during loading.
Then run Lighthouse against the same URL and save its report. Lighthouse is a lab test, so its score is useful for comparing runs under similar conditions rather than predicting every visitor’s experience. In a worked example, suppose the report measures Largest Contentful Paint (LCP) at 3.4 seconds and Cumulative Layout Shift (CLS) at 0.18. Those are illustrative local measurements, not claims about your site. Google’s published “good” thresholds are LCP of 2.5 seconds or less and CLS of 0.1 or less; use them as reference points, while treating the change between your own runs as the immediate test.
Read Web Performance Optimization Tips for Faster Applications after saving that first trace; I disagree with choosing fixes from a tips list beforehand because it cannot tell you which request delays your page. Record the date, URL, build commit, LCP element, and two suspected causes in a plain text file. That note is your baseline: without it, an apparent improvement may simply be a different page, build, or browser state.
A local command is enough to make the baseline repeatable
You do not need a dedicated server to run the first test. Build the app as you normally deliver it, then serve the built files locally rather than testing a development server, because development builds often load different JavaScript and assets. If your build writes to dist, run npx http-server@14 ./dist -p 4173 in one terminal. For a different output directory, change that path. Install Chrome and have Node.js available before running the following shell script; save it as check-performance.sh and invoke it with sh check-performance.sh.
#!/bin/sh
set -eu
URL="${1:-http://localhost:4173/}"
mkdir -p reports
npx --yes lighthouse@12 "$URL" \
--chrome-flags="--headless" \
--only-categories=performance \
--output=json \
--output-path=reports/baseline.json
node -e 'const r=require("./reports/baseline.json"); console.log({lcp:r.audits["largest-contentful-paint"].displayValue,cls:r.audits["cumulative-layout-shift"].displayValue})'
The script writes a Lighthouse JSON report and prints two values you can copy into your baseline note. Pass another local URL as its first argument to test a specific route. Run it a few times before editing code, since a single lab result can move with background activity on your laptop. Keep the full JSON from one run and the range of values from the repeats; the range makes a tiny later change less tempting to call a win.
Use the Chrome DevTools Performance panel for the question the summary cannot answer: what was the browser doing while the visitor waited? Inspect the LCP marker, the preceding network requests, and long tasks on the main thread. Lighthouse cannot provide a meaningful local Interaction to Next Paint (INP) result from a page load alone, because INP concerns actual interactions. If the site has enough traffic to show field data, check its URL or origin in PageSpeed Insights, which can display Chrome UX Report data; otherwise, test the important tap or click yourself in DevTools and avoid presenting a lab score as field evidence.
My starting budget would be at most 170 KB of compressed first-route JavaScript, a value to tune for the app rather than a universal limit. Record the current amount before adopting it: if a small static portfolio already sends far less, that cap would permit needless growth. The useful budget is one that flags a change you could realistically investigate before delivery.
The first fix should remove a visible delay, not add a service
Return to the waterfall and change one cause of the slow first view. If the LCP element is a hero image, export it near its rendered dimensions, try AVIF or WebP, and compare the actual files in the browser. Give the image explicit width and height attributes to reserve space. Do not apply loading=”lazy” to that above-the-fold image, because deferring the image needed for LCP can delay the very paint you are trying to improve. Images farther down the page are better candidates for lazy loading.
If CSS delays the paint, inspect whether the initial route needs every rule in its stylesheet. Remove unused styles from the shipped build before adding a more elaborate delivery mechanism, because fewer blocking bytes are simpler to verify than a new pipeline. If JavaScript occupies the main thread, use DevTools to identify the long task and the module behind it. A calendar that appears only after a visitor opens the booking form may be a candidate for a dynamic import(); the same calendar should not be deferred if opening it is the page’s immediate purpose.
Fonts deserve a similarly specific decision. A self-hosted WOFF2 file with font-display: swap can keep text visible while the font loads, but a font swap may still move headings if the fallback has different dimensions. Check CLS after the change rather than assuming the configuration solved it. For a small client site, using a system font may win on simplicity when the custom typeface has no contractual or design value, because it removes a network request and the swap behavior altogether.
Set cache headers only after separating versioned assets from HTML. Hashed files such as app.a1b2.js can use Cache-Control: public, max-age=31536000, immutable because a content change produces a new URL. HTML should have a policy that allows updates to become visible promptly, because long-lived cached HTML may point to assets you have already removed. These are HTTP behavior decisions, not Lighthouse-score tricks: test a second visit in DevTools with the cache enabled to see whether the browser reuses what you expected.
Read Web Performance Optimization Tips for Modern Apps after checking the blocking request; I would still fix that request before adopting a broader modern-app checklist, because an unmeasured change can add work without improving this page. After each edit, rebuild, run the same local URL again, and note both the result and any visible regression. Stop when the visitor’s task improves or the next proposed fix costs more time than the delay warrants.
Direct hosting wins until a CDN solves a measured problem
There is a real choice between serving the built site directly from its current host and putting Cloudflare’s free plan in front of it. Direct hosting wins when the origin responds quickly for the client’s audience and its cache headers work, because it has fewer settings to maintain; its cost is that distant visitors may fetch every asset from that origin. Cloudflare wins when repeat tests from relevant locations show slow asset delivery and its edge cache improves it; its cost is DNS changes, cache rules to check, and another place to diagnose stale content. A free plan does not make those maintenance costs disappear.
I would not add a CDN immediately after seeing a poor Lighthouse score, because a CDN cannot shorten a long JavaScript task running in the visitor’s browser. Check the Network panel’s Timing view first. If time to first byte is consistently high from the location that matters, test the deployed page through the proposed setup before committing to it. If the delay sits between a downloaded script and the next paint, return to the code instead.
Also compare like with like after deployment. Test the same route and build, verify the HTML and asset cache responses, and inspect the browser console. A faster local run paired with a broken deployed form is not a successful optimization. Keep the baseline note with the project so the next client-requested feature has a known starting point rather than an inherited mystery.
The next useful step is one saved run
Choose the client page you would be least comfortable demonstrating on a phone. Build it, serve the output locally, run the script, and save its JSON report before changing anything. Write down the LCP element and the request or task immediately before it appears. That single observation is enough to choose a first edit—and to tell whether the edit earned its place.


