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

推荐订阅源

量子位
Vercel News
Vercel News
Google DeepMind News
Google DeepMind News
罗磊的独立博客
WordPress大学
WordPress大学
The Cloudflare Blog
GbyAI
GbyAI
The Register - Security
The Register - Security
L
LangChain Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Microsoft Security Blog
Microsoft Security Blog
MyScale Blog
MyScale Blog
A
About on SuperTechFans
U
Unit 42
T
The Blog of Author Tim Ferriss
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
Visual Studio Blog
云风的 BLOG
云风的 BLOG
Stack Overflow Blog
Stack Overflow Blog
博客园 - 三生石上(FineUI控件)
博客园 - 司徒正美
Blog — PlanetScale
Blog — PlanetScale
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
C
CERT Recently Published Vulnerability Notes
T
The Exploit Database - CXSecurity.com
T
Tor Project blog
A
Arctic Wolf
H
Hacker News: Front Page
NISL@THU
NISL@THU
F
Full Disclosure
雷峰网
雷峰网
L
LINUX DO - 热门话题
Recent Announcements
Recent Announcements
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
Google Online Security Blog
Google Online Security Blog
I
InfoQ
Webroot Blog
Webroot Blog
S
Security Affairs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
爱范儿
爱范儿
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
N
News | PayPal Newsroom
Forbes - Security
Forbes - Security
博客园 - Franky
V
Vulnerabilities – Threatpost
博客园 - 【当耐特】
Latest news
Latest news

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Lighthouse 100 and Still Crashes: OOM Explained
Ashish Kumar · 2026-05-07 · via DEV Community

A Lighthouse score of 100 means the page loads efficiently in a clean, synthetic test environment; it says nothing about what happens after 6 hours of continuous use — a hundred route navigations, thousands of polling cycles, and a React Query cache holding responses that should have been evicted hours ago.

Why lab metrics miss this: Lighthouse runs a single page load on a clean browser instance. It cannot measure heap growth rate, memory fragmentation, or tab survival probability on a 3GB Android device. OOM tab crashes don't produce JavaScript errors — the renderer process just dies.

What this covers: What OOM crashes actually are and why Chrome's tab limits differ by device, why long-lived SPAs accumulate memory (route state, unbounded caches, subscription leaks), how to monitor heap growth in production, and strategies that keep apps stable over 8-hour sessions.

Diagram contrasting Lighthouse lab scores with long-session memory growth and OOM risk that lab tools do not capture.


The Lab vs Field Gap

Lighthouse runs a single, synthetic page load on a clean browser instance with simulated throttling. It measures what happens during that load LCP, CLS, TBT, FID, and overall performance and produces a score. For what it measures, it's accurate and useful.

What it doesn't measure:

Not measured by Lighthouse Why it matters
Memory growth over time Long-lived apps accumulate heap
Memory after 100 navigations SPAs never fully unload between routes
GC pressure under sustained load Polling loops cause GC pauses over hours
Memory fragmentation Old generation becomes fragmented; GC is less effective
Tab survival on low-RAM devices Chrome kills tabs proactively on 2GB-RAM phones

The Core Web Vitals field data in CrUX (Chrome User Experience Report) does capture real users, but it captures the load experience of those users, not their session survival rate. There's no CrUX metric for "tab was killed by the OS after 4 hours." That data simply doesn't exist in any aggregate form you can easily access.

This creates a dangerous blind spot. You can achieve a perfect Lighthouse score and still be shipping software that degrades and crashes for users who use it the way internal tools get used all day, every day, with the same tab.)


What an OOM Tab Crash Actually Is

OOM stands for out of memory. In Chrome, each tab runs in its own renderer process with a memory limit. When the heap grows beyond that limit, Chrome's renderer terminates the process and shows the user an "Aw, Snap!" error page.

The limit isn't a fixed number. Chrome sets it dynamically based on:

  • The total available system RAM
  • How many other tabs are open
  • Chrome's own heuristics for memory pressure

On a desktop machine with 16GB RAM and only a few tabs open, a single tab can use 2-4GB before Chrome kills it. On a mobile device with 3GB RAM running multiple apps, Chrome may proactively kill background tabs (not even the active one) when the system is under memory pressure.

The performance.memory API (Chrome only, non-standard) exposes some of these limits:

if (performance.memory) {
  console.log({
    // Total heap size V8 has allocated (committed memory)
    totalJSHeapSize: performance.memory.totalJSHeapSize,
    // How much of that heap is actually in use by JS objects
    usedJSHeapSize: performance.memory.usedJSHeapSize,
    // The hard limit  heap cannot grow beyond this
    jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
  });
}

Enter fullscreen mode Exit fullscreen mode

On a typical desktop Chrome, jsHeapSizeLimit is around 4GB. On Android Chrome on a 3GB device, it's often 512MB-1GB. When usedJSHeapSize approaches jsHeapSizeLimit, the tab is on the edge of an OOM crash.


Mobile RAM: The Real Constraint

The operations team using the dashboard I mentioned were on desktop machines. But the majority of web traffic globally is on mobile, and mobile RAM constraints are severe.

Chrome on Android uses the following rough thresholds for tab killing (these aren't official numbers they're inferred from behavior and reported by developers and researchers):

Device RAM Approx. tab heap limit Background tab kill threshold
1 GB ~150-200MB ~50MB
2 GB ~250-350MB ~100MB
3 GB ~400-500MB ~200MB
4 GB+ ~700MB+ ~300MB

On a 2GB Android device still common in many markets a tab that uses 300MB of heap will be proactively killed by Chrome when the user switches to another app. When they switch back, Chrome reloads the tab from scratch, losing all state. Users experience this as "the page keeps refreshing."

For apps that need to survive on mobile, 150-200MB is a realistic heap budget to aim for in steady state. This is much tighter than desktop, and it rules out certain architectural decisions like caching every API response in memory indefinitely.


Why Long Sessions Break Apps That Pass Lighthouse

Accumulated Route State

React Router and Next.js don't fully unmount pages on navigation in SPAs. The framework manages route transitions, but unless you're explicitly code-splitting and unloading modules, the JavaScript for visited routes stays loaded. More importantly, React components that were mounted may hold state in closures, context, or stores.

Consider a dashboard with 20 different report views. After visiting all 20, the user has accumulated the component trees, data structures, and side effects of all 20 pages. If any of those pages has a memory leak (even a small one), it compounds with each visit.

Cache Without Eviction

React Query and SWR are excellent data fetching libraries. Their defaults are also memory-addictive for long-running apps. React Query, by default, keeps cached data in memory for 5 minutes after it's no longer actively used. In a dashboard that queries 200 unique order IDs over a day of use, that's 200 cached responses held in memory simultaneously.

// React Query default: caches everything for 5 minutes
const queryClient = new QueryClient();

// Tuned for long-lived apps
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 30_000,     // data is fresh for 30s
      gcTime: 60_000,        // remove from cache after 1 minute of disuse (was cacheTime in v4)
    },
  },
});

Enter fullscreen mode Exit fullscreen mode

Reducing gcTime from 5 minutes to 1 minute sounds minor. Over 8 hours of active use, it's the difference between holding thousands of cached responses and holding only the last few minutes of data.

Event Listener and Subscription Accumulation

Even small leaks compound over time. An event listener that's not removed on component unmount is tiny maybe a few KB of retained closures. After 500 route navigations (common in a full workday), those small leaks add up to hundreds ofMB.

I covered this in detail in the React Memory Leaks post, but the time dimension is what makes it a production crash issue vs a development curiosity. In a unit test or Lighthouse run, the component mounts once and unmounts once. In 8 hours of use, it might mount and unmount 300 times.


Diagnosing Production OOM

OOM crashes are hard to diagnose because they don't produce a JavaScript error. The renderer process just dies. There's no stack trace, no error boundary trigger, no Sentry event.

Field signals to watch for:

  • Session duration distribution: if your analytics shows a spike in session ends at 3-4 hours, users are being kicked out (either by OOM or by giving up on a slow app)
  • Navigation abandonment: if users stop navigating after a certain number of route changes, the app may be degrading
  • performance.memory logging: log usedJSHeapSize periodically (every 5 minutes) to your analytics service. Over time this builds a picture of heap growth rate per user type
// Log memory usage every 5 minutes to your analytics service
function startMemoryMonitoring() {
  if (!performance.memory) return; // Chrome only

  setInterval(() => {
    const { usedJSHeapSize, jsHeapSizeLimit } = performance.memory;
    const usagePercent = (usedJSHeapSize / jsHeapSizeLimit) * 100;

    analytics.track("memory_usage", {
      usedMB: Math.round(usedJSHeapSize / 1024 / 1024),
      limitMB: Math.round(jsHeapSizeLimit / 1024 / 1024),
      usagePercent: Math.round(usagePercent),
    });

    // Warn if approaching limit
    if (usagePercent > 80) {
      console.warn(`Heap at ${usagePercent.toFixed(0)}% of limit`);
    }
  }, 5 * 60 * 1000);
}

Enter fullscreen mode Exit fullscreen mode

The PerformanceObserver type: 'memory' API (Chrome M89+) provides a more event-driven approach:

if (PerformanceObserver.supportedEntryTypes.includes("memory")) {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      console.log("Memory entry:", entry);
    }
  });
  observer.observe({ type: "memory" });
}

Enter fullscreen mode Exit fullscreen mode

As of 2026 this API is still Chrome-only and not widely used, but it's the right long-term direction for memory monitoring.


Strategies for Long-Lived Apps

Route-Based Code Splitting and Proper Unmounting

React Router v6's <Outlet> and lazy loading ensure that route components are code-split. But code-splitting only controls the initial load once a module is loaded, it stays in memory. The more important mechanism is ensuring components fully unmount and release their state.

In practice this means auditing that route components don't hold global references, that their useEffect cleanups fire correctly, and that stores don't accumulate data from visited routes indefinitely.

LRU Cache Eviction

For any client-side cache, implement maximum size with LRU (least recently used) eviction:


const detailCache = new LRUCache({
  max: 50,                     // keep at most 50 items
  ttl: 1000 * 60 * 5,          // expire items after 5 minutes
  maxSize: 50 * 1024 * 1024,   // cap at 50MB total
  sizeCalculation: (value) => JSON.stringify(value).length,
});

Enter fullscreen mode Exit fullscreen mode

lru-cache is the standard choice in the Node/browser ecosystem well-maintained, tiny, fast. For React Query, use the gcTime option as shown earlier.

Page Visibility API: Reduce Background Work

When the user switches tabs or minimizes the browser, there's no reason to keep polling APIs at full speed. The Page Visibility API lets you detect visibility changes:

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    // Tab is hidden  reduce or stop background work
    queryClient.pauseMutations();
    stopPolling();
  } else {
    // Tab is visible again  resume
    queryClient.resumeMutations();
    startPolling();
    queryClient.invalidateQueries(); // fetch fresh data
  }
});

Enter fullscreen mode Exit fullscreen mode

Stopping API polling when the tab is hidden also prevents stale data from accumulating in caches that aren't being actively evicted.

Idle Reload for Kiosks and Operational Dashboards

For dashboards that need to be 100% reliable over long periods, the nuclear option is a scheduled reload during periods of inactivity. This sounds crude but is genuinely effective for kiosk displays, NOC dashboards, and ops tooling:

function scheduleIdleReload(idleMinutes = 120) {
  let idleTimer;

  function resetTimer() {
    clearTimeout(idleTimer);
    idleTimer = setTimeout(() => {
      // User has been inactive for `idleMinutes`  reload the page
      window.location.reload();
    }, idleMinutes * 60 * 1000);
  }

  // Reset on any user interaction
  ["mousedown", "keydown", "touchstart", "scroll"].forEach((event) => {
    document.addEventListener(event, resetTimer, { passive: true });
  });

  resetTimer();
}

// Reload if idle for 2 hours
scheduleIdleReload(120);

Enter fullscreen mode Exit fullscreen mode

With a 2-hour idle reload, a tab that's been left open all night reloads when the ops team comes in the morning. Fresh state, no accumulated heap. Combined with proper session restoration (saving the current view to sessionStorage), this is invisible to users.


A Memory Budget for Long-Running SPAs

Based on my experience with internal tools and operations dashboards, here's a rough budget that keeps long-running apps stable:

Category Desktop Target Mobile Target
Initial page load heap < 50MB < 30MB
Steady-state heap (after 1 hour) < 150MB < 80MB
Maximum acceptable heap < 400MB < 150MB
API response cache size < 50MB < 20MB

If your app exceeds the "maximum acceptable" column, it's at elevated risk of OOM on the target device class. Use performance.memory logging in production to measure where your real users actually land.

Lighthouse will never tell you about these numbers. It can't it doesn't run for an hour. But they're what determines whether your users can actually use your app through a full working day.

Reducing React Query gcTime from 5 minutes to 90 seconds, pausing polling on tab hide, and deploying an idle reload (4-hour threshold) are the three changes that keep most long-lived dashboards stable. Heap stays under 200MB for the full working day. Lighthouse score stays 100. It just isn't the metric that matters for this class of problem.


Read the original article on Renderlog.in:
https://renderlog.in/blog/lighthouse-100-still-crashes-memory/

If you found this helpful, I've also built some free tools for developers and everyday users. Feel free to try them once:

JSON Tools: https://json.renderlog.in
Text Tools: https://text.renderlog.in
QR Tools: https://qr.renderlog.in