Skip to main content
Case Studies

A Fast Website Has to Stay Fast at Every Release

How to move from a Lighthouse check on pull requests to a performance gate before production, with measurement budgets, failure handling, and evidence tied to the build being released. A process design grounded in the current limits of Enersys’s delivery system.

Web PerformanceLighthouse CIRelease EngineeringCore Web Vitals

A prospective customer opens a service page on their phone to see whether a company handles the work they need. The headline takes time to appear. When they reach for the contact button, an image loads and pushes it down the screen.

This hypothetical visit contains several different problems: waiting for content, waiting for a response, and losing the position of something the visitor intended to tap. A page can contain the right information while making people work to reach it.

For a software company such as Enersys, speed is a quality the website should retain throughout its life. One optimization project cannot settle the issue. A new image, form, or tracking script can add the cost back in the next release.

The useful question for a delivery team is this: when today’s change makes the website slower, who finds out before the customer, and where can the system stop that release?

Start with evidence from the earlier workflow

The earlier workflow used as the baseline before the October 28, 2026 change runs when a pull request opens or receives new changes. Defined in .github/workflows/lighthouse.yml, it builds a static export and audits /th locally three times.

The file sets errors below 0.60 for Performance and 0.80 for Accessibility. Best Practices and SEO below 0.80 produce warnings. It attempts to retain reports as an artifact for 30 days and returns Lighthouse CI’s exit code to the audit step.

A real GitHub Actions run, 39057394135, completed successfully in October 2026. The reviewed steps include Checkout, Build static export, Serve & audit, and Upload Lighthouse results. This provides execution evidence for the pull request workflow. It does not establish branch protection or production release enforcement. The public illustration records those facts without reproducing private repository, account, or runner details.

The production path needs its own qualification. The current Rancher and Harbor release process includes manual approval, but its deployment does not depend on this Lighthouse CI check. The repository also retains a release.yml triggered by v* tags for the older DigitalOcean path. That file cannot stand in as evidence of the current production process.

This article separates the earlier PR audit from the candidate preflight that has now been added and tested, then explains the remaining work to make every production release depend on its result. The stricter budget example and image-regression scenario later in the article illustrate decisions; they are not Enersys measurements.

What Enersys added to the candidate preflight

This work adds a candidate audit tool and PR/manual workflow in commit 6ec17848, based on the source used in production. Compared with the earlier workflow, it centralizes policy, checks required files and size limits before starting Chrome, and binds the result to a fingerprint of the static export.

The representative set covers five page types in Thai and English, making ten URLs: the homepage, e-Withholding Tax service, the e-Withholding Tax versus e-Filing article, the long page-data/shared-JavaScript case study, and the e-Withholding Tax readiness utility. Every URL receives three mobile simulated runs.

Actual new policy Threshold
Performance and Accessibility Median at least 0.75 and 0.80, respectively
Per-page gzip document / directly referenced JavaScript / directly referenced CSS At most 50,000 / 300,000 / 40,000 bytes
JavaScript transferred in the browser audit Worst run at most 300,000 bytes and 20 scripts
CSS transferred in the browser audit Worst run at most 40,000 bytes
LCP and TBT Warnings above 4,000 ms and 600 ms, respectively; these do not block the gate

The two size-check layers serve different purposes. The first reads emitted files and directly referenced dependencies. The browser layer observes transfers during Lighthouse. Their figures should not be mixed. The tool pins Lighthouse CI 0.15.1 and Lighthouse 12.6.1.

The scope qualification matters: this is an implemented local/PR preflight. The manual Harbor/Rancher path does not yet require it before every deployment. Building an audit tool and closing production bypasses are separate steps.

The complete ten-URL, thirty-run check passed on October 28, 2026, at 03:55 Bangkok time. It audited a static export whose source had been verified as f21ae899, the production source baseline before this article. It did not audit the running production image or the later build containing this article.

Page Median Performance, TH / EN Median LCP, TH / EN
Homepage 86 / 87 4,213 / 4,065 ms
e-Withholding Tax service 87 / 83 3,987 / 3,761 ms
e-Withholding Tax versus e-Filing article 83 / 84 4,659 / 4,504 ms
Long page-data/shared-JavaScript article 86 / 88 4,112 / 3,957 ms
Readiness utility 89 / 91 3,758 / 3,538 ms

Every URL had a median Accessibility score of 96. Performance scores above are converted from the 0-to-1 scale to 0-to-100 for readability. The measurement used Chrome 156, Lighthouse 12.6.1, a 412 × 823 mobile viewport, DPR 1.75, and simulated throttling with 150 ms RTT, 1,638.4 Kbps throughput, and 4× CPU slowdown. Keep that profile with the results.

The results also show work that remains. Several LCP medians exceed 4,000 ms, but those are warnings in this policy and do not stop the preflight. Passing a baseline regression floor is not evidence of good field Core Web Vitals.

The tool binds the 1,817-file export to the SHA-256 below and checks that the files and policy do not change during the audit. Using this result for a release requires verifying the receipt against the same files again before packaging or deployment. Rebuilding would invalidate the old result. The receipt is local process evidence, not a signed provenance attestation.

743e4881aa7966bb06de5bb4c9b8d133d41069ec9189c5d90347a6d9fa8eaa37

Two failure checks used an oversized synthetic HTML candidate and actual baseline Lighthouse reports with a temporarily stricter minScore of 1.0. Both returned exit code 1 and prevented the following sentinel command from running. This proves the test command sequence stopped; it was not a failed deployment experiment in the live cluster. Redacted measurement summary

Earlier PR auditing compared with the passing local preflight, with production enforcement explicitly still separate
Original illustration based on the earlier PR execution and the isolated candidate preflight of ten URLs and thirty runs. Mandatory Harbor/Rancher integration and post-release monitoring remain proposed. Status as of October 28, 2026. This is not a dashboard screenshot.

Measure the experience before choosing a score

Core Web Vitals define good thresholds of no more than 2.5 seconds for LCP, 200 ms for INP, and 0.1 for CLS, assessed at the 75th percentile. These address main content appearing, interaction response, and visual stability. Core Web Vitals thresholds

Those thresholds set a direction for actual experience. Delivery work needs two kinds of evidence.

Lab data supports comparisons before release under controlled conditions. Field data describes actual visits. A Lighthouse navigation audit uses TBT to help diagnose main-thread blocking during page load. TBT is not INP and cannot establish responsiveness throughout a visit. Why lab and field data differ

Our proposed division of work is to use the lab to detect changes before release, then field data to check what reaches users. If the lab improves while visitors still struggle, the investigation stays open. A better score alone is not a reason to close it.

Choose pages by the work visitors do

Auditing /th is a manageable starting point. It cannot represent every page. A lightweight homepage may say little about a service page with a map, video, or contact form.

Before adding URLs, list representative pages by workload. The homepage covers shared navigation. A service page covers the route into a business enquiry. A long article exercises images and fonts. A form page needs interaction checks. Select real, maintained URLs and record the risk each one represents.

A bilingual site also needs representative Thai and English pages. Heading length, fonts, and content can differ. A passing Thai page does not establish a passing English equivalent.

In this design, each pull request runs a small, stable set of important pages. Scheduled checks rotate through the rest. A change to a shared component, such as navigation or a contact form, expands the checks to affected page groups.

Give design decisions a performance budget

A performance budget is an agreed limit before a team adds more work to a page. It can cover display time, blocking work, or JavaScript transfer size. An image or widget becomes easier to assess when its cost has a visible allowance.

These are illustrative, stricter starting points than the actual policy above. Measure a baseline under a defined test profile before adopting them. They are not measurements of Enersys production.

Lab check Illustrative proposed limit How it informs a decision
Lighthouse Performance At least 0.85 An overall signal, followed by the metrics that changed
LCP At most 3,500 ms Main content under the specified test profile
TBT At most 300 ms Blocking work during page load
CLS At most 0.1 Shifts observed during the test
JavaScript transferred on initial load At most 250 KiB compressed in this example A separate size check with explicit URL and cache state

The illustrative 3,500 ms lab limit does not change the 2.5-second Core Web Vitals good threshold. Passing a load-time CLS check also cannot establish that nothing shifts after a visitor scrolls or opens another part of the page.

The 250 KiB budget needs a definition of what it includes. A report of uncompressed file size and a report of network transfer size cannot be compared directly as a regression.

Start with a limit the team can maintain and set a date to tighten it. A threshold that needs an exception on every release will quickly lose its purpose.

Make the decision rule readable

This illustrative lighthouserc.cjs assumes the static export has already been built. Its stricter values are proposals. A production implementation should pin Lighthouse CI, Chrome, and dependencies.

module.exports = {
  ci: {
    collect: {
      staticDistDir: './out',
      url: [
        'http://localhost/th',
        'http://localhost/en',
      ],
      numberOfRuns: 3,
    },
    assert: {
      assertions: {
        'categories:performance': ['error', {
          minScore: 0.85,
          aggregationMethod: 'median',
        }],
        'largest-contentful-paint': ['error', {
          maxNumericValue: 3500,
          aggregationMethod: 'median',
        }],
        'total-blocking-time': ['error', {
          maxNumericValue: 300,
          aggregationMethod: 'median',
        }],
        'cumulative-layout-shift': ['error', {
          maxNumericValue: 0.1,
          aggregationMethod: 'median',
        }],
        'categories:accessibility': ['error', {
          minScore: 0.80,
          aggregationMethod: 'median',
        }],
      },
    },
    upload: {
      target: 'filesystem',
      outputDir: './lighthouse-results',
    },
  },
};

Here, error makes a threshold failure return a non-zero exit code. Explicit median aggregation uses the middle value for each metric across three runs. The last run in a manifest should not be presented as the aggregate decision. Lighthouse CI configuration

The JavaScript budget in the table is not enforced by this snippet. It needs a separate check using the agreed size definition. The Accessibility floor of 0.80 is carried over from the existing workflow; it is not certification of accessibility or proof that every task works with a keyboard and screen reader.

Pinning versions helps distinguish a page change from a tool change. When upgrading the tools, compare the old and new baseline before changing thresholds. Avoid upgrading the measurement system and weakening its criteria together, because the cause of any difference becomes harder to identify.

Work through a candidate that fails

Suppose a service page replaces its main image and adds an external widget. Every figure in this example is invented to explain the decision. None is an Enersys result.

Metric Three baseline runs Three candidate runs Candidate median
Performance 0.88, 0.89, 0.87 0.87, 0.83, 0.84 0.84
LCP 2,800, 2,900, 2,850 ms 3,700, 3,600, 3,900 ms 3,700 ms
TBT 140, 160, 150 ms 170, 210, 190 ms 190 ms

The candidate fails the illustrative Performance and LCP criteria even though its best score is 0.87. Showing only that run would leave the approver with incomplete evidence. TBT remains within budget, so this table alone does not justify declaring JavaScript the main cause.

The next step is to inspect the LCP report and network waterfall. Did the LCP element change? How large is the image? When did the browser request it? When did the widget compete for resources? Form a hypothesis, change one factor, such as sizing the image for its displayed dimensions, then measure again under the same conditions.

For the developer, match the LCP element to its resource URL. Separate waiting for HTML, waiting to request the resource, loading it, and waiting to render it. A late request calls for checking whether the initial page exposes the image to the browser. A large file calls for sizing and compression work. A loaded but hidden element calls for inspecting JavaScript work or reveal conditions. Change the part the trace identifies, rerun, and attach the diff and new candidate reports. Breaking down LCP

If the fix brings the candidate within budget, attach the new reports to the new candidate. A result from the previous files cannot certify later changes, even if the change is one line.

Separate a failed threshold from an invalid test

A test machine competing for CPU with another build can produce unstable results. Chrome failing to start, or the page returning 404, can make the audit unusable. This design holds the release in both cases, but assigns different work.

For a reproducible threshold failure, the change owner investigates the regression. For an infrastructure failure, the test-system owner restores the environment and reruns the same candidate.

Agree on retry policy before a failure occurs. An example is one full retry when there is evidence of an infrastructure problem, preserving the first reports and the reason. If measurements remain unstable, pause the decision and repair the measurement system. Repeatedly running until one result is green removes the point of a threshold.

Report collection should still execute after an assertion failure, while the final status retains the failure. Successfully uploading an artifact does not mean the audit passed. A skipped check or absent report should not count as a pass either.

Connect the evidence to the release permission

Our proposal is to carry evidence with the build from candidate creation through production.

  1. Identify the source commit, lockfile, and tool versions, then build the release candidate.
  2. Audit that candidate’s output. For a container release, test files extracted from the same image or run that image in the test environment.
  3. Save every URL’s reports with the commit and artifact or image digest.
  4. Allow production approval only after all required results are present and passing, then release the same artifact by its tested digest.
  5. Check the live pages and connect field observations back to the release.

If audit and deployment jobs share a GitHub Actions workflow, needs can express a dependency. The design also needs artifact handoff and candidate identity checks. A job called Lighthouse does not create that relationship on its own. GitHub Actions job dependencies

When another system performs deployment, including the current manual path, that system must verify a passing result for the same digest before approval. Otherwise, the pull request result remains information that the deployment can bypass.

An emergency exception should record its approver, reason, accepted risk, expiry, and follow-up work. The release record must retain the exception as an exception, so it cannot later be described as an ordinary gate pass.

Keep watching after production receives visitors

Testing local output covers what the team ships. It does not cover DNS, CDN behavior, caches, third-party services, and visitors’ devices. The proposed process adds a production URL check after deployment to catch environmental differences.

Scheduled checks between releases also matter: a third party can change behavior without a commit in our repository. Keep the URLs, test profile, schedule, and alert owner consistent enough to compare. Distinguish an unavailable page from a possible performance regression before routing the alert.

For field data, track p75 by page type and device when sample size permits. Put the observation window and release dates on the same chart. A dataset containing several releases cannot immediately establish the effect of the latest one.

With CrUX, check whether the result represents a URL or an origin and whether enough data is available. CrUX has user and popularity eligibility requirements. Missing data is not evidence that a website is fast or slow. CrUX methodology

If collecting measurements directly, keep the metrics and context that help diagnose performance, such as page group and release identifier, with data handling appropriate to the service. Attaching the text someone entered into a form does not improve a speed measurement.

Put evidence in front of the promise

A release approver should be able to open the record and identify the tested pages, candidate, criteria version, failures, exceptions, and rollback artifact. Content owners should see which budget a new image or embed consumes. Operations owners should know where each alert goes.

Speed helps visitors reach information and continue their task. A faster measurement does not establish a particular revenue increase; that outcome needs its own measurement. Google also states that good Core Web Vitals results do not guarantee top search rankings. Google Search guidance on page experience

For a team starting today, choose one important page and collect a repeatable baseline. Then deliberately create a failing candidate in a test environment. If the normal production path can still release it, the first task is to connect that evidence to the permission to deploy.

For the next 30 days, we propose a named engineering owner for the baseline and weekly regression review, an operations owner for scheduled-check failures and production incidents, and a release approver who records exceptions. The content owner reviews new images and embeds against the budget. At day 30, these owners should review failed checks, retry reasons, response times, exception expiry, and field-data coverage, then decide whether to tighten the budget or fix measurement gaps. This is a proposed ownership plan, not a claim that these duties have already been assigned at Enersys.

"Empowering Innovation,
Transforming Futures."

Contact us to make your project a reality.