惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
GbyAI
GbyAI
T
Tor Project blog
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 聂微东
Stack Overflow Blog
Stack Overflow Blog
V
Vulnerabilities – Threatpost
小众软件
小众软件
Y
Y Combinator Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
量子位
U
Unit 42
K
Kaspersky official blog
Cisco Talos Blog
Cisco Talos Blog
The GitHub Blog
The GitHub Blog
博客园 - 【当耐特】
P
Proofpoint News Feed
腾讯CDC
C
Cisco Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Threat Research - Cisco Blogs
Latest news
Latest news
云风的 BLOG
云风的 BLOG
月光博客
月光博客
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tenable Blog
Google DeepMind News
Google DeepMind News
Simon Willison's Weblog
Simon Willison's Weblog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
L
LINUX DO - 热门话题
宝玉的分享
宝玉的分享
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
The Last Watchdog
The Last Watchdog
PCI Perspectives
PCI Perspectives
T
Tailwind CSS Blog
N
Netflix TechBlog - Medium
H
Heimdal Security Blog
博客园_首页
Recent Commits to openclaw:main
Recent Commits to openclaw:main
阮一峰的网络日志
阮一峰的网络日志
AWS News Blog
AWS News Blog
Hacker News: Ask HN
Hacker News: Ask HN
Blog — PlanetScale
Blog — PlanetScale
D
DataBreaches.Net
N
News | PayPal Newsroom
Microsoft Security Blog
Microsoft Security Blog
O
OpenAI News
M
MIT News - Artificial intelligence

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution Node.js Cluster Mode: Scaling the Event Loop Across CPU Cores Postgres Partial Indexes: Stopping Soft Deletes from Ruining Your Query Performance Request Coalescing with the Singleflight Pattern: Stop Drowning Your Database on Every Cache Miss The Bulkhead Pattern: Why One Slow Endpoint Should Not Drown Your Whole Service Node.js AsyncLocalStorage: End-to-End Request Context Without the Propagation Hell Postgres Deadlocks: Logging the Victim, Reproducing the Race, and Fixing the Lock Order Your Node.js HTTP Client Is the Bottleneck: Connection Pool Tuning That Works Optimistic Locking in Postgres: Stop Losing Data to Race Conditions Postgres Read Replicas: Stop Serving Stale Data to Your Users Cursor Pagination: Why Offset Queries Explode at Scale and How to Fix Them Node.js Worker Threads: 60 Lines That Stop a CSV Upload from Timing Out Every Other Request Reliable Webhook Delivery: Architecture for Outbound HTTP You Can Trust Request Timeouts and Deadline Propagation: Stop the Chain of Slowness Advanced Security Practices in Node.js Graceful Shutdown in Node.js: The 40 Lines That Stop 502s During Deploys Finding Node.js Memory Leaks with Heap Snapshots Idempotency Keys in 30 Lines: Stop Your Webhook From Charging Customers Twice Backpressure In Node.js: The Fix For Slow-Motion Queue Meltdowns Retries Done Right: Jitter, Budgets, and the Stampede You Did Not See Coming The Cache Stampede: Why Your "Just Add Redis" Layer Crashes Postgres at 3 a.m. Postgres SKIP LOCKED: An 80-Line Job Queue You Can Run Without Redis Stop Doing Work Nobody Wants: AbortController in Node.js, Done Right The N+1 Query Problem: We Found 23 In One Codebase And Killed Every One I Tried 5 AI Coding Tools for a Month. Here Is What I Actually Use CI/CD From Zero to Production in 30 Minutes With GitHub Actions Node.js vs Bun vs Deno: Which Runtime Should You Pick in 2025? Kubernetes Resource Requests And Limits: The Numbers That Decide If Your Cluster Is Stable The Three Pillars of Observability Are A Myth: What Actually Matters In Production pnpm Vs npm Vs yarn Vs Bun For Monorepos: Which One Earns The Migration In 2024 JSONB Indexing In Postgres: GIN Vs Expression Indexes, And When Each Is The Right Choice A Code Review Checklist That Ends The Same Three Arguments Every Sprint gRPC Vs REST In 2024: When The Switch Pays For Itself React Suspense For Data Fetching: The Pattern That Replaces Half Your Loading State Code The Five-Stage Rollout: How To Ship A Risky Change Without Holding Your Breath GitHub Actions In A Monorepo: Caching, Path Filters, And Secret Boundaries That Actually Work The Blameless Postmortem That Actually Improves Things: A Template And Six Hard-Won Rules Recursive CTEs In Postgres: How To Query A Tree Without N Round Trips Node.js Streams: When They Actually Help, And When They Just Add Complexity Playwright Vs Cypress In 2024: The Honest Comparison Of Which One Earns The Test Time React Server Components: The Mental Model That Makes The "use client" Boundary Obvious Pod Disruption Budgets: The K8s Object That Keeps Your Service Up During Cluster Maintenance Postgres LISTEN/NOTIFY: The Pub/Sub You Already Have And Are Not Using Chaos Engineering Starter Kit: The Five Drills That Don't Need Netflix-Scale Spec-Driven API Development With OpenAPI: How To Stop Drifting From Your Docs Kubernetes Autoscaling Beyond CPU: The Custom-Metric HPA Pattern That Actually Works Postgres Partitioning For Time-Series: The Boring Setup That Saves Your Database Distributed Locks With Redis: An Honest Look At Redlock And When You Don't Need It HTTP/2 vs HTTP/3: What Actually Changes For Your App, And What Doesn't Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want Kafka vs RabbitMQ: A Decision Tree That Doesn't Hate You UUID vs Bigint Primary Keys In Postgres: The Index Math That Decides For You Flame Graphs: How To Find The Slow Function In 30 Seconds Without Profiling Theatre Postgres Streaming Vs. Logical Replication: Which One Solves Your Actual Problem ESLint Rules That Earn Their Keep: The Twelve I Enable On Every Project Pre-Commit Hooks That Pay For Themselves: Husky, lint-staged, And The Five Rules That Stick Zero-Downtime Database Migrations: The Six-Step Pattern That Rules Them All Circuit Breakers In Node.js: 50 Lines That Stop A Failing Dependency From Taking Down Your Service Postgres VACUUM Is Not Magic: How Your Hot Table Bloats To 80GB And How To Fix It Kubernetes Liveness And Readiness Probes: The Difference That Causes Half Your Outages Rate Limiting In Production: A Token Bucket In 30 Lines Of Redis The Outbox Pattern: How To Stop Losing Events When Postgres And Kafka Disagree Load Testing With k6: The Three Scenarios That Find Real Bugs (Not Synthetic Numbers) Postgres Row-Level Security For Multi-Tenant Apps: The Pattern That Stops You From Leaking Data Rebase vs. Merge: The Team Policy That Ends The Argument Forever OpenTelemetry in Node.js: Distributed Tracing That Actually Helps During an Incident Feature Flags That Pay Rent: The 4 Flag Types And When To Delete Each ETag, Last-Modified, and the Caching Headers Most APIs Get Wrong Connection Pooling Without the Cargo Cult: pgbouncer in 100 Lines of Config JSONB Is Not a Schema: When To Reach For It in Postgres, And When To Stop Bash Strict Mode: The Three Lines That Stop Your Deploy Script From Lying To You
Lighthouse CI: Automate Performance Budgets to Catch Regressions Before They Deploy
The Practica · 2026-06-16 · via The Practical Developer

Performance regressions are insidious. A single commit adds a 30 KB analytics script. Nobody notices in dev because the dev server runs on localhost with no throttling. The PR gets approved based on screenshots. Three weeks later, Lighthouse in production shows a 12-point drop in Performance score, and your mobile users have been bouncing off a page that takes 8 seconds to become interactive.

You cannot catch this with code review. You cannot catch it with unit tests. You can only catch it by running a real browser against your deployed page, measuring the same metrics that Google uses for ranking, and asserting that they stay above a hard threshold.

That is what Lighthouse CI gives you. A CI pipeline that runs Lighthouse, compares results against a defined budget, and fails the build when the site gets slower. No guesswork. No “we will fix it later.” No 3 a.m. alerts.

Why you need a performance budget, not just a score

The Lighthouse Performance score is a weighted composite of six metrics: FCP, LCP, TBT, CLS, SI, and TTI. A single score is useful for a quick sanity check, but it masks regressions. You can drop 200 ms on LCP and lose 3 points, but if TBT improves by 50 ms you might net out at the same score. The PR passes, and your LCP has silently regressed.

A performance budget flips this. You set hard numeric thresholds for each metric:

  • LCP under 2.5 seconds (on Mobile throttled)
  • TBT under 200 ms
  • CLS under 0.1
  • Total JS bundle under 250 KB (compressed)
  • Total page weight under 500 KB

These do not average out. Each one is an absolute gate. Exceed any threshold and the build fails. The developer who added the 30 KB script gets a red checkmark and a link to the Lighthouse report showing exactly which asset pushed them over.

Setting up Lighthouse CI

Lighthouse CI (@lhci/cli) runs Lighthouse headlessly and exposes an assert API for budgets. You install it in your project, configure a budget file, wire it into your CI workflow, and you are done.

npm install --save-dev @lhci/cli

Create a .lighthouserc.js config file at the project root:

module.exports = {
  ci: {
    collect: {
      // Run against the production build served locally
      staticDistDir: './dist',
      // Or against a deployed URL:
      // url: ['https://staging.example.com'],
      numberOfRuns: 3,
      settings: {
        preset: 'desktop',
        // For mobile budgets, use:
        // preset: 'desktop' with custom throttling, or the mobile preset
      },
    },
    assert: {
      assertions: {
        // Performance score floor
        'categories:performance': ['error', { minScore: 0.9 }],

        // Metric budgets (mobile-emulated)
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'total-blocking-time': ['error', { maxNumericValue: 200 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'first-contentful-paint': ['error', { maxNumericValue: 1800 }],
        'interactive': ['error', { maxNumericValue: 3500 }],
        'speed-index': ['error', { maxNumericValue: 3000 }],

        // Resource size budgets
        'resource-summary:script:transfer': ['error', { maxNumericValue: 250 }],
        'resource-summary:stylesheet:transfer': ['error', { maxNumericValue: 50 }],
        'resource-summary:total:transfer': ['error', { maxNumericValue: 600 }],

        // Warn on common anti-patterns instead of failing
        'unminified-javascript': ['warn'],
        'unused-javascript': ['warn', { maxLength: 0 }],
        'offscreen-images': ['warn'],
        'uses-responsive-images': ['warn'],
        'uses-http2': ['off'],
      },
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
};

Break down what is happening here.

The collect phase builds your site or starts a server and runs Lighthouse against it. numberOfRuns: 3 means Lighthouse runs three times and takes the median, which smooths out variance from the CI runner’s background noise.

The assert phase is where the budget lives. Each assertion has a severity (error fails the build, warn prints a warning) and a threshold. The maxNumericValue is the hard cap for the metric in milliseconds or kilobytes.

The upload phase sends the results somewhere so you can view the full Lighthouse report. temporary-public-storage stores the report on a public URL that expires after 7 days. In production you would point this at your own storage or use the LHCI server for historical comparisons.

The complete GitHub Actions workflow

Here is the full workflow that builds your site, runs Lighthouse CI, and posts the results as a PR comment.

Create .github/workflows/lighthouse.yml:

name: Lighthouse CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build the site
        run: npm run build

      - name: Run Lighthouse CI
        run: |
          npx lhci autorun --config=./lighthouserc.js
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

      - name: Save Lighthouse report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: lighthouse-report
          path: .lighthouseci/
          retention-days: 14

The LHCI_GITHUB_APP_TOKEN secret is optional but recommended. Without it, LHCI cannot post status checks and detailed reports to your PRs. To get the token, install the Lighthouse CI GitHub app on your repo. If you skip the token, the workflow still runs and fails on budget violations, but the results live only in the workflow logs and the uploaded artifact.

Budgeting for real projects

The numbers in the example config are aggressive. They assume a well-optimized static site or SSR app. If your app starts above those thresholds, do not set the budgets to match your current numbers. That locks in mediocrity. Instead, set budgets that are 10% better than where you are today, and tighten them every sprint.

Here is a phased approach for a team that is still optimizing:

Phase 1 (month one): Set budgets to warn, not error. Get the team used to seeing Lighthouse output in CI without blocking deploys. Discuss the results in standup.

assertions: {
  'largest-contentful-paint': ['warn', { maxNumericValue: 3500 }],
  'total-blocking-time': ['warn', { maxNumericValue: 400 }],
}

Phase 2 (month two): Tighten to error levels that match your current median. Any regression from the baseline fails the build.

Phase 3 (month three): Set budgets that represent your target, not your baseline. Tighten monthly.

The mistake most teams make is skipping Phase 1. They set aggressive budgets on day one, the CI pipeline turns red on every PR, developers start ignoring the results, and within two weeks the budget is deleted from the config. Start with warnings, build the habit, then tighten.

Handling the common CI gotchas

Chrome is missing or crashes

The GitHub Actions ubuntu-latest runner ships with Chrome, but you need to ensure it is in the PATH. LHCI typically finds it automatically. If you get a “Chrome could not be found” error, install it explicitly:

- name: Install Chrome
  run: |
    sudo apt-get update
    sudo apt-get install -y google-chrome-stable

Throttling inconsistency between CI runs

CI runners are noisy neighbors. CPU stealtime and memory pressure vary from run to run. Three mitigations help:

  1. Run Lighthouse three times and take the median (numberOfRuns: 3). This filters out the outlier where your CI runner happened to be compiling a kernel module in the background.

  2. Use settings.throttlingMethod: 'devtools' in your config instead of simulated throttling. DevTools throttling is more deterministic than the default simulated approach.

  3. Budgets should have buffer. If your LCP is consistently 1.8 seconds on local, budget for 2.5 seconds in CI. The 700 ms cushion absorbs runner noise while still flagging a real 900 ms regression.

Budget fails on the first run but passes on retry

This is runner noise, not a real regression. Use numberOfRuns: 5 and set assertionLevel: 'warn' for metrics that are close to the boundary. Then add a secondary job that runs weekly against production with stricter budgets and no retries. The weekly job is the source of truth.

Viewing reports

When the workflow finishes, the Lighthouse report artifact contains an HTML report for each URL tested. Download it from the Actions tab and open it in a browser. The report shows every audit, the metrics, the filmstrip, and the opportunities section with specific recommendations.

If you installed the LHCI GitHub app, every PR gets a status check with a summary of the budget results and a link to the full report. Example output looks like this:

✅ Performance: 94 (budget: 90)
✅ LCP: 2.1 s (budget: 2.5 s)
✅ TBT: 140 ms (budget: 200 ms)
✅ CLS: 0.05 (budget: 0.1)
✅ JS size: 187 KB (budget: 250 KB)
❌ Total page weight: 623 KB (budget: 600 KB)

The failing line tells you exactly which budget you blew and by how much. No ambiguity. The developer knows “my page weight is 23 KB over budget” and can look at their PR diff to find the culprit.

Adding Lighthouse CI to an Astro project (this blog)

This blog runs on Astro. The setup is identical to what is shown above, with one adjustment: Astro’s default build output goes to dist/, which is what staticDistDir: './dist' expects. If you use the output: 'server' mode (SSR), switch to startServerCommand instead:

collect: {
  startServerCommand: 'npm run preview',
  url: ['http://localhost:4321'],
  numberOfRuns: 3,
},

This starts the Astro preview server on port 4321, runs Lighthouse against it, and shuts the server down when the collection finishes.

Going further: Budgets for non-performance metrics

Lighthouse audits more than speed. The assertions block also supports accessibility, SEO, and best-practice categories. You can set budgets for those too:

assertions: {
  'categories:accessibility': ['error', { minScore: 0.95 }],
  'categories:seo': ['error', { minScore: 0.95 }],
  'categories:best-practices': ['error', { minScore: 0.9 }],
  'aria-allowed-attr': ['error'],
  'document-title': ['error'],
  'meta-description': ['error'],
  'viewport': ['error'],
}

Accessibility budgets are particularly valuable because they catch regressions that visual review misses. A new component that renders a <div> instead of a <button> passes code review but fails an accessibility audit. The PR gets blocked until the component uses the correct semantic element.

The production audit workflow

The per-PR workflow catches new regressions. But you also need a regular production audit that tracks trend data over time. Add a scheduled workflow that runs weekly against production with the same budget configuration:

name: Production Lighthouse Audit

on:
  schedule:
    - cron: '0 6 * * 1'  # Every Monday at 6 AM
  workflow_dispatch:      # Manual trigger

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - name: Run Lighthouse against production
        run: |
          npx lhci autorun \
            --config=./lighthouserc.production.js
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

The production config uses a different collect setup:

collect: {
  url: ['https://your-production-site.com'],
  numberOfRuns: 5,
  settings: {
    preset: 'desktop',
    throttlingMethod: 'devtools',
  },
},

Five runs instead of three, because production traffic adds variance that more samples smooth out. The results get uploaded and compared against the historical trend in the LHCI dashboard.

What budgets to start with

If you are setting this up right now and do not know what numbers to pick, use the Web Almanac’s 2024 median as your starting point. For mobile pages:

MetricMedian p50Good (p75)Great (p90)
LCP2.8 s2.5 s1.8 s
TBT300 ms200 ms100 ms
CLS0.150.10.05
FCP2.2 s1.8 s1.2 s
JS (compressed)350 KB250 KB150 KB

Start with “Good” as your error threshold. Tighten to “Great” over three months. This gives you room to ship features while the team adapts to the constraint.

Takeaway

Performance is a feature, and like any feature it needs tests. Lighthouse CI gives you those tests: a browser running against your actual build, measuring the same six metrics that determine your Core Web Vitals score, and asserting they stay above a hard threshold. The config file is 30 lines. The CI workflow is another 20. The setup takes an afternoon.

The alternative is what you have now: a production dashboard that blinks red at 3 a.m., a pager that goes off, and a frantic revert of a commit that shipped two weeks ago. Pick the approach that makes performance the CI’s job, not the SRE’s problem.

A note from Yojji

Setting up automated performance pipelines that catch regressions before they reach production requires more than just running a tool in CI. It demands careful budget calibration, understanding how your framework and hosting affect each Lighthouse metric, and ongoing maintenance as your application grows. Yojji’s engineering teams regularly build and maintain this kind of performance infrastructure for their clients, from CI/CD optimization to full-cycle product delivery across the JavaScript ecosystem and cloud platforms on AWS, Azure, and Google Cloud. Yojji is an international custom software development company founded in 2016, with offices in Europe, the US, and the UK, providing senior engineering talent that treats performance as a first-class requirement, not an afterthought.