learning-website-nextjs1-4 Exercise 3: Scripts Inside a Fragment, Tested in a Real Browser ======================================================================================= 1,844 of the 4,415 pages have a `; export default async function LabPage({ params }: { params: Promise<{ name: string }> }) { if (process.env["LW_LAB"] !== "1") notFound(); const { name } = await params; return (

Lab: {name}

); } Save as apps/languages/app/lab/page.tsx: import Link from "next/link"; import { notFound } from "next/navigation"; /** A test bench for scripts inside fragments. It exists only in a build made with LW_LAB=1. */ export default function Lab() { if (process.env["LW_LAB"] !== "1") notFound(); return (

Lab

plain (scripts not re-run)

rerun (scripts re-run)

); } It is driven by a real headless Chrome through the DevTools protocol, using only Node's own WebSocket: Save as lab-test.mjs: // Drive a real (headless) Chrome through the DevTools protocol: do scripts inside fragments run // (a) when the page is opened directly, and (b) when the visitor arrives by clicking a link? // node lab-test.mjs (the languages app built with LW_LAB=1 and started) import { spawn } from "node:child_process"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const base = process.argv[2] ?? "http://localhost:3001"; const chrome = spawn("C:/Program Files/Google/Chrome/Application/chrome.exe", [ "--headless=new", "--disable-gpu", "--remote-debugging-port=9333", `--user-data-dir=${mkdtempSync(join(tmpdir(), "lw-chrome-"))}`, "about:blank", ], { stdio: "ignore" }); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); let targets; for (let i = 0; i < 40; i++) { try { targets = await (await fetch("http://127.0.0.1:9333/json")).json(); if (targets.length) break; } catch { /* not up yet */ } await sleep(250); } const ws = new WebSocket(targets.find((t) => t.type === "page").webSocketDebuggerUrl); await new Promise((resolve) => ws.addEventListener("open", resolve)); let id = 0; const pending = new Map(); ws.addEventListener("message", (event) => { const message = JSON.parse(event.data); if (message.id && pending.has(message.id)) { pending.get(message.id)(message); pending.delete(message.id); } }); const send = (method, params = {}) => new Promise((resolve) => { const n = ++id; pending.set(n, resolve); ws.send(JSON.stringify({ id: n, method, params })); }); const evaluate = async (expression) => (await send("Runtime.evaluate", { expression, returnByValue: true })).result?.result?.value; async function open(path) { await send("Page.enable"); await send("Page.navigate", { url: base + path }); await sleep(2000); } const text = () => evaluate('document.getElementById("out") ? document.getElementById("out").textContent : "(no #out)"'); const rows = []; await open("/lab/plain"); rows.push(["open /lab/plain directly", await text()]); await open("/lab/rerun"); rows.push(["open /lab/rerun directly", await text()]); await open("/lab"); await evaluate('document.getElementById("to-plain").click()'); await sleep(2500); rows.push(["click a link to /lab/plain", `${await text()} [address now ${await evaluate("location.pathname")}]`]); await open("/lab"); await evaluate('document.getElementById("to-rerun").click()'); await sleep(2500); rows.push(["click a link to /lab/rerun", `${await text()} [address now ${await evaluate("location.pathname")}]`]); // the same link twice in one window, going back in between: the script runs once per visit await open("/lab"); await evaluate('document.getElementById("to-rerun").click()'); await sleep(2000); await evaluate("history.back()"); await sleep(1500); await evaluate('document.getElementById("to-rerun").click()'); await sleep(2000); rows.push(["click, back, click again", `${await text()} (the count is per window, so 2 is right)`]); for (const [what, result] of rows) console.log(what.padEnd(30), result); ws.close(); chrome.kill(); process.exit(0); What happened, in order. FIRST VERSION (RunScripts re-created every script it found): open /lab/plain directly script ran 1 time(s) on this page open /lab/rerun directly script ran 2 time(s) on this page <-- a bug click a link to /lab/plain waiting <-- the trap: the script never ran click a link to /lab/rerun script ran 1 time(s) on this page What went wrong: the component also re-ran the scripts on the page that was downloaded directly, where the browser had already run them, so the script ran TWICE. A script that adds a button's click handler would have added it twice. I would not have found this without opening the page in a browser and counting. How to avoid it: re-run scripts ONLY for pages that arrived by navigation, and test both ways of arriving, with a script that counts how often it ran. THE FIX: HydrationMark (in the layout) sets window.__lwHydrated after the first page is hydrated. A child's effect runs before its parent's, so on the first page RunScripts sees the flag unset (the page was downloaded: skip) and on every later page it sees it set (the page came by navigation: re-run). Two markers (data-lw-initial, data-lw-ran) stop React's development mode, which runs every effect twice, from running a script twice. SECOND VERSION: open /lab/plain directly script ran 1 time(s) on this page open /lab/rerun directly script ran 1 time(s) on this page click a link to /lab/plain waiting [address now /lab/plain] (still the trap: no component, no run) click a link to /lab/rerun script ran 1 time(s) on this page [address now /lab/rerun] click, back, click again script ran 2 time(s) on this page (the count is per window, so 2 is right: once per visit) What was NOT tested: React's development mode (its double effects are handled by the markers but "next dev" was not run); scripts that load other scripts with src= (the copy keeps the attributes, but none was tried); and other browsers (only Chrome). WHY THIS WORKS AS AN ANSWER --------------------------- The behaviour that matters (does it run, and how many times, depending on how the visitor arrived) was measured in a real browser, a bug was found and fixed that reading the code would not have shown, and both outcomes are in the record.