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

推荐订阅源

博客园 - 叶小钗
Last Week in AI
Last Week in AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
雷峰网
雷峰网
GbyAI
GbyAI
Hugging Face - Blog
Hugging Face - Blog
N
Netflix TechBlog - Medium
博客园 - 聂微东
Y
Y Combinator Blog
罗磊的独立博客
博客园_首页
小众软件
小众软件
有赞技术团队
有赞技术团队
爱范儿
爱范儿
F
Fortinet All Blogs
C
Check Point Blog
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
Apple Machine Learning Research
Apple Machine Learning Research
M
MIT News - Artificial intelligence
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
aimingoo的专栏
aimingoo的专栏

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
Detecting YouTube SPA Navigation in a Chrome Extension Co...
SHOTA · 2026-05-15 · via DEV Community

SHOTA

YouTube is a single-page application. The page never fully reloads when you click a video. The URL changes, the DOM updates, but DOMContentLoaded fires exactly once — when Chrome first loads the tab.

This breaks the most natural assumption in Chrome extension development: that your content script runs once per page. For YouTube, "once per page load" means once for the entire session.

I hit this while building LectureLoop — an extension that injects a side panel and loop replay controls onto YouTube lecture videos. When a user navigates from one video to another, the extension needs to reset, re-detect the video element, and reinitialize its state. Without handling SPA navigation, it just… stops working after the first video.

The Problem with window.location

The naive fix is to watch window.location.href for changes. Chrome content scripts can read window.location freely. The issue is timing: YouTube uses the History API (history.pushState) to update the URL. There's no event you can listen to directly — popstate only fires on back/forward navigation, not on pushState calls.

You could poll with setInterval, but polling is inaccurate by definition and wastes CPU even when nothing is happening.

MutationObserver on document.body

The approach that actually works is watching document.body for DOM subtree changes and comparing the URL on each mutation:

let lastUrl = location.href;

const navObserver = new MutationObserver(() => {
  if (location.href !== lastUrl) {
    lastUrl = location.href;
    setTimeout(init, 1500);
  }
});

navObserver.observe(document.body, { childList: true, subtree: true });

window.addEventListener('pagehide', () => navObserver.disconnect(), { once: true });

Enter fullscreen mode Exit fullscreen mode

The setTimeout(init, 1500) gives YouTube's React app time to finish rendering the new video element before init() tries to query it. Without the delay, document.querySelector('video') returns null — YouTube has updated the URL but hasn't yet replaced the DOM.

The pagehide cleanup matters: if the tab is closed or navigated away from, you want to disconnect the observer rather than leaving it running against an unmounted document.

The Loop Replay Implementation

Once the content script reliably re-runs on navigation, implementing loop replay is straightforward. The side panel sends a message to the content script to jump to a specific timestamp:

chrome.runtime.onMessage.addListener((msg) => {
  if (msg.type === 'JUMP_TO_TIMESTAMP' && typeof msg.sec === 'number') {
    const video = document.querySelector('video');
    if (video) video.currentTime = msg.sec;
  }
});

Enter fullscreen mode Exit fullscreen mode

The side panel tracks the loop start and end points. When the video's timeupdate event fires and the current time exceeds the loop end, it sends JUMP_TO_TIMESTAMP with the loop start time. This gives users a way to replay a specific section of a lecture repeatedly without scrubbing the timeline by hand.

Why Not scripting.executeScript?

An alternative is to use chrome.scripting.executeScript from the background service worker instead of keeping long-lived listeners in a content script. The tradeoff: executeScript requires the scripting permission, which triggers a warning in the Chrome Web Store review process about "reading browsing history." Using a content script with matches: ["https://www.youtube.com/*"] avoids this — it's a declared, static permission that reviewers understand at a glance.

It's a minor point, but CWS reviewers look at permissions carefully. Keeping the injection in the content script declaration rather than calling executeScript from background kept the permission list clean.

Lessons

Three things I'd do differently on the next extension that needs SPA navigation handling:

  1. Set the URL guard at the start of init(), not just in the observer. If init() is async and runs while the user clicks to a third video, you can end up with two concurrent initializations racing each other.

  2. Use a cancellation token. Start each init() with an incrementing ID, and bail out of the async steps if a newer ID has been issued.

  3. Log every navigation event in development. YouTube's SPA navigation is fast and the delay is imprecise — having a visible indicator that the observer fired makes the timing problem much easier to debug.


🔗 LectureLoop on Chrome Web Store: Install