<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: scott winter</title>
    <description>The latest articles on DEV Community by scott winter (@scott_winter_77ced0700c92).</description>
    <link>https://dev.to/scott_winter_77ced0700c92</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3822616%2F61a67a6c-e974-4135-883d-5332cc7d26e9.png</url>
      <title>DEV Community: scott winter</title>
      <link>https://dev.to/scott_winter_77ced0700c92</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/scott_winter_77ced0700c92"/>
    <language>en</language>
    <item>
      <title>Using Codex to Improve Web Performance: From a Lighthouse Score of 76 to 98</title>
      <dc:creator>scott winter</dc:creator>
      <pubDate>Sat, 05 Sep 2026 07:46:52 +0000</pubDate>
      <link>https://dev.to/scott_winter_77ced0700c92/using-codex-to-improve-web-performance-from-a-lighthouse-score-of-76-to-98-9g8</link>
      <guid>https://dev.to/scott_winter_77ced0700c92/using-codex-to-improve-web-performance-from-a-lighthouse-score-of-76-to-98-9g8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9bgv7o0vmv0oy1chqw92.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9bgv7o0vmv0oy1chqw92.webp" alt="From unstable hydration to predictable rendering" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;an AI complete a web-performance optimization project on its own?&lt;/p&gt;

&lt;p&gt;It can do much of the work, but the effective workflow is not to tell Codex “optimize this site” and blindly accept every edit it proposes. A better division of labor is to let the developer own the goal, constraints, and final decisions, while Codex handles the repetitive, context-heavy work: running Lighthouse, inspecting DevTools, tracing data through the source code, testing hypotheses, and running the test suite. Codex brings the evidence back; the developer decides what it means.&lt;/p&gt;

&lt;p&gt;This article follows that process through a real Next.js fruit puzzle game. The first mobile Lighthouse run in Chrome DevTools scored &lt;strong&gt;76&lt;/strong&gt;. A saved rerun under the same general setup scored &lt;strong&gt;79&lt;/strong&gt;. After one round of diagnosis and targeted changes with Codex, the local production build reached &lt;strong&gt;98&lt;/strong&gt;. Total Blocking Time (TBT) fell from &lt;strong&gt;828.5 ms&lt;/strong&gt; to &lt;strong&gt;49 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part is not simply which lines changed. The case study answers a more useful set of questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How should a performance problem be described to Codex?&lt;/li&gt;
&lt;li&gt;How do you make it investigate before it starts editing?&lt;/li&gt;
&lt;li&gt;How can a developer tell whether an AI-found cause is actually the root cause?&lt;/li&gt;
&lt;li&gt;When is it safe to let Codex implement the plan?&lt;/li&gt;
&lt;li&gt;How do you improve a Lighthouse score without damaging the experience of real users?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Start by dividing responsibility
&lt;/h2&gt;

&lt;p&gt;Codex is particularly useful for work such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;running Lighthouse repeatedly and saving machine-readable JSON reports;&lt;/li&gt;
&lt;li&gt;finding long tasks in a Performance trace;&lt;/li&gt;
&lt;li&gt;checking the Console, Network panel, DOM, and response headers;&lt;/li&gt;
&lt;li&gt;tracing the origin of a value across the repository;&lt;/li&gt;
&lt;li&gt;writing a small benchmark for a suspicious function;&lt;/li&gt;
&lt;li&gt;implementing a narrowly defined code change; and&lt;/li&gt;
&lt;li&gt;running builds, unit tests, and end-to-end tests and summarizing the results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The developer still owns three decisions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define the product behavior that must not change.&lt;/strong&gt; In this example, the board must still contain 170 cells, analytics events must not disappear, and mobile interactions must remain usable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide whether the evidence supports the conclusion.&lt;/strong&gt; A Lighthouse recommendation is not automatically a root cause, and correlation is not causation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set the optimization boundary.&lt;/strong&gt; Should third-party analytics be delayed? Is a different caching strategy acceptable? Is another two points worth adding runtime or operational complexity?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without this separation, an agent can produce a technically impressive score improvement that is not a valid product change.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. First prompt: establish a baseline, and do not edit code
&lt;/h2&gt;

&lt;p&gt;The first request should explicitly forbid code changes. Without a saved baseline, there is no reliable way to show that later work helped. Changing images, JavaScript, caching, and components all at once also makes the result impossible to attribute.&lt;/p&gt;

&lt;p&gt;Here is the shape of the initial request used in this investigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Analyze this website's performance with Chrome DevTools and Lighthouse in mobile mode.

The current performance score is about 76. Do not modify the code yet. First:
1. Save a reproducible Lighthouse JSON baseline.
2. Record FCP, LCP, Speed Index, TBT, TTI, CLS, long tasks, and transfer size.
3. Check the Console for hydration warnings and runtime errors.
4. Inspect the Network panel for large resources, third-party scripts, and oversized images.
5. Connect browser observations to source code, but present conclusions as hypotheses for now.
6. For every conclusion, include the evidence, likely impact, and a way to verify it.

Do not treat a generic Lighthouse recommendation as the root cause.
Do not remove product functionality to improve the score.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first saved rerun looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCP&lt;/td&gt;
&lt;td&gt;1.66 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;1.66 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed Index&lt;/td&gt;
&lt;td&gt;2.82 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TBT&lt;/td&gt;
&lt;td&gt;828.5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTI&lt;/td&gt;
&lt;td&gt;5.91 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max Potential FID&lt;/td&gt;
&lt;td&gt;421 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server response&lt;/td&gt;
&lt;td&gt;663 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total byte weight&lt;/td&gt;
&lt;td&gt;474 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The original 76 and the saved 79 do not contradict each other. A single Lighthouse run is sensitive to CPU scheduling, network emulation, and whether an edge worker or cache is cold or warm. At this stage, the shape of the metrics matters more than a three-point difference.&lt;/p&gt;

&lt;p&gt;The strongest signal was the combination of a reasonably quick LCP (&lt;strong&gt;1.66 s&lt;/strong&gt;) with very high TBT (&lt;strong&gt;828.5 ms&lt;/strong&gt;) and a TTI close to six seconds. The screen appeared, but the main thread kept doing expensive JavaScript work. Compressing an image might reduce transfer size, but it could not explain more than 800 ms of main-thread blocking.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Second prompt: make Codex build an evidence chain
&lt;/h2&gt;

&lt;p&gt;Once the TBT anomaly is visible, the next request should not be “optimize the JavaScript.” Ask where the blocking time came from, and keep the investigation read-only.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The baseline has LCP 1.66 s, TBT 828.5 ms, and TTI 5.91 s.
Continue investigating the main-thread blocking, but do not edit code yet.

Requirements:
1. List every long task over 50 ms in the Performance trace.
2. Connect each long task to a script, component initialization, or business function.
3. Check for Console errors in the same time window.
4. Compare the key DOM data in the server HTML with the data after hydration.
5. If a business function looks suspicious, benchmark it separately.
6. Summarize the result as observation -&amp;gt; evidence -&amp;gt; hypothesis -&amp;gt; verification.

If the evidence is incomplete, label the result as a hypothesis rather than a conclusion.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trace revealed two consecutive React long tasks, approximately &lt;strong&gt;421 ms&lt;/strong&gt; and &lt;strong&gt;403 ms&lt;/strong&gt;. Together they accounted for most of the TBT. At the same time, the Console showed the shortened React error &lt;strong&gt;#418&lt;/strong&gt;, which indicates that the server-rendered markup did not match the client's first render: a hydration mismatch.&lt;/p&gt;

&lt;p&gt;That error alone is not proof that hydration caused all 800 ms of blocking. Three further checks were needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which content actually differed between the server and client?&lt;/li&gt;
&lt;li&gt;Was the difference large enough for React to abandon the existing DOM?&lt;/li&gt;
&lt;li&gt;Was the cost of rebuilding that UI comparable to the long tasks in the trace?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Codex read the board generated in the server HTML and the board present after hydration, then calculated a small checksum for each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;server board: &lt;code&gt;63227&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;client board: &lt;code&gt;69345&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both boards were valid 10×17 boards—&lt;strong&gt;170 cells&lt;/strong&gt;—but they were not the same board. React could not reuse the server nodes and rebuilt the board subtree.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxulmy75pl6i5dym9reql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxulmy75pl6i5dym9reql.png" alt="Evidence chain for the hydration mismatch" width="799" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Codex then benchmarked the board generator in isolation. On an unrestricted local CPU, the median was about &lt;strong&gt;82 ms&lt;/strong&gt;, with the slowest sample around &lt;strong&gt;121 ms&lt;/strong&gt;. Lighthouse emulates a slower mobile CPU, so a cost in the 300–400 ms range is plausible. That was consistent with the two long tasks in the trace.&lt;/p&gt;

&lt;p&gt;The evidence now formed a closed loop:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;th&gt;What it tells us&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lighthouse&lt;/td&gt;
&lt;td&gt;TBT 828.5 ms&lt;/td&gt;
&lt;td&gt;The main thread is heavily blocked.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance trace&lt;/td&gt;
&lt;td&gt;421 ms and 403 ms long tasks&lt;/td&gt;
&lt;td&gt;The blocking is concentrated in React's initial work.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Console&lt;/td&gt;
&lt;td&gt;React #418&lt;/td&gt;
&lt;td&gt;The server and first client render disagree.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOM data&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;63227&lt;/code&gt; ≠ &lt;code&gt;69345&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The two sides really rendered different boards.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source tracing&lt;/td&gt;
&lt;td&gt;Server uses a cached previous-day seed; client uses today's seed&lt;/td&gt;
&lt;td&gt;The data split has been located.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Function benchmark&lt;/td&gt;
&lt;td&gt;82–121 ms locally&lt;/td&gt;
&lt;td&gt;The generator is large enough to become a mobile long task.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a much more actionable explanation than “there are too many React components” or “the bundle is too large.” It also leads to an experiment that can disprove it: make the first board identical on both sides and see whether the mismatch and long tasks disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The root cause was data ownership, not randomness
&lt;/h2&gt;

&lt;p&gt;The game was intended to generate one stable daily board. The simplified implementation looked deterministic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;board&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBoard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;seedFromToday&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;renderPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// client's first render&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createBoard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;seedFromToday&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same algorithm appears on both sides, so why would the results differ? The page was using a day-long server cache.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server HTML could come from a cache entry created yesterday, with yesterday's seed.&lt;/li&gt;
&lt;li&gt;Hydration ran now, so the client immediately calculated today's seed.&lt;/li&gt;
&lt;li&gt;When the date boundary and cache lifetime did not line up, the two renders produced different boards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The subtle lesson is that sharing an algorithm does not mean sharing the input to the first render. Deterministic hydration requires the same data, not merely the same function. If the inputs come from the current clock, a random source, a time zone, or cache state, the result can diverge even when the code is identical.&lt;/p&gt;

&lt;p&gt;The developer therefore has to decide who owns the initial data. In this case, the server became the sole owner: it generated the initial board and passed it to the client. The client stopped trying to infer the first screen for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Third prompt: design the solution before implementing it
&lt;/h2&gt;

&lt;p&gt;After the root cause is established, ask Codex for a layered plan. Make it separate the root-cause fix from the work that merely reduces the remaining cost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We confirmed that the hydration mismatch comes from a cached server board and a
client board generated from the current date. React discards and rebuilds 170 cells.

Do not edit code yet. Design an optimization plan first.
1. Fix the split in first-screen data ownership before adding memoization.
2. Separate root-cause, rendering, resource, and third-party-script changes.
3. For each change, predict which Lighthouse metrics should move.
4. List product behavior that could change and the regression risks.
5. Give an implementation order and a verification method for each stage.

Constraints: keep the 170-cell board, daily-game behavior, analytics events,
and existing interactions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extra planning step is worthwhile because performance work usually crosses rendering, caching, resources, and third-party code. If editing starts immediately, too many areas can change at once, making risk and causality difficult to evaluate.&lt;/p&gt;

&lt;p&gt;The resulting plan had four layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Consistent first render:&lt;/strong&gt; generate one initial board on the server and pass it to the client as props.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stable runtime rendering:&lt;/strong&gt; memoize the 170-cell board and reduce the countdown update frequency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource delivery:&lt;/strong&gt; replace the oversized logo and serve AVIF screenshots at an appropriate size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party work:&lt;/strong&gt; queue analytics immediately, but defer the full GA SDK until the page is idle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A useful prioritization question is: &lt;strong&gt;does this remove unnecessary work, or merely make unnecessary work a little faster?&lt;/strong&gt; Unifying the first-screen data removes work and therefore comes first. &lt;code&gt;memo&lt;/code&gt; is valuable, but it is a secondary optimization if the application is still rebuilding the wrong board.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Fourth prompt: implement in causal stages
&lt;/h2&gt;

&lt;p&gt;Only after reviewing the plan should the developer authorize code changes. Each stage should be isolated so its effect can be tested.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Implement the approved plan in four stages.

Stage 1: fix first-screen data ownership and the hydration mismatch.
Stage 2: reduce repeated board rendering and timer updates.
Stage 3: optimize the logo and game images.
Stage 4: defer third-party analytics loading.

After every stage:
1. Run the relevant tests and a production build.
2. Check the browser Console for warnings and errors.
3. Explain the change, the expected metric impact, and the observed result.
4. Stop if a regression appears; do not continue to the next stage.

Do not change the game rules. Do not hide content or delete analytics to improve Lighthouse.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.1 Fix the hydration data flow
&lt;/h3&gt;

&lt;p&gt;The essential shape of the fix was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The server owns the first-screen data.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initialSeed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PREVIEW_SEED&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initialBoard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBoard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialSeed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Game&lt;/span&gt; &lt;span class="nx"&gt;initialSeed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;initialSeed&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;initialBoard&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;initialBoard&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// The client uses that data during hydration and does not generate it again.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initialSeed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialBoard&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setBoard&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialBoard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="nx"&gt;initialSeed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialBoard&lt;/span&gt;&lt;span class="p"&gt;]]))&lt;/span&gt;

  &lt;span class="c1"&gt;// Later resets, daily synchronization, and board changes still follow&lt;/span&gt;
  &lt;span class="c1"&gt;// the existing product rules.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the preview board was fixed, the first screen no longer needed to depend on a date-based ISR result. Static generation also removed the cache-creation date from the hydration equation.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2 Stop re-rendering the board for unrelated timer ticks
&lt;/h3&gt;

&lt;p&gt;There is no reason for all 170 cells to render again whenever the countdown changes. The board became a memoized component, and the timer moved from 50 ms updates to 100 ms updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Board&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Board&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderCell&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updateTimer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One hundred milliseconds is still smooth enough for a countdown display while cutting the number of timer updates in half.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.3 Deliver images at their displayed size
&lt;/h3&gt;

&lt;p&gt;The initial report found a 512×512 PNG logo rendered at roughly 56×56 pixels. About 26.3 KB was being transferred for pixels that could never be seen. A game screenshot weighed about 67 KB and could be reduced by another 14 KB.&lt;/p&gt;

&lt;p&gt;The final version used a sub-1 KB vector logo and responsive image delivery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 760px)"&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"screenshot-384.avif"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"screenshot-640.avif"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"640"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"474"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Game screen"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image-delivery audit no longer reported an avoidable-size warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.4 Move the full analytics SDK off the critical path
&lt;/h3&gt;

&lt;p&gt;The initial GA transfer was about &lt;strong&gt;166.9 KB&lt;/strong&gt;, and its main-thread execution cost was about &lt;strong&gt;155 ms&lt;/strong&gt;. The requirement to record the first page view does not necessarily mean that the entire SDK must download and initialize before the first screen is usable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create a queue immediately so the initial event is not lost.&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="nf"&gt;queueInitialPageView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Download the full SDK once the page has settled.&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;requestIdleCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loadAnalytics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also a product decision. If ad attribution or experiment assignment must complete before the first interaction, analytics cannot simply be deferred. If events only need to arrive eventually, queuing them first and loading the SDK during idle time is a better trade-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Fifth prompt: prove that the optimization worked
&lt;/h2&gt;

&lt;p&gt;Codex can run the checks, but the acceptance criteria should be stated up front. A score by itself is not enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Validate the optimization. Do not report only the Lighthouse score.

1. Run the production build, unit tests, and E2E tests.
2. Serve the production build and rerun mobile Lighthouse with the same settings.
3. Compare FCP, LCP, Speed Index, TBT, TTI, CLS, server response, and transfer size.
4. Check that no hydration warning or error remains in the Console.
5. Verify the 170-cell board, start/reset flow, countdown, and mobile layout.
6. Report regressions as well as improvements; do not cherry-pick favorable numbers.
7. Clearly separate local results from results measured after deployment.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The measured results were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (online rerun)&lt;/th&gt;
&lt;th&gt;After (local production)&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;+19 points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCP&lt;/td&gt;
&lt;td&gt;1.66 s&lt;/td&gt;
&lt;td&gt;0.89 s&lt;/td&gt;
&lt;td&gt;-46.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;1.66 s&lt;/td&gt;
&lt;td&gt;2.37 s&lt;/td&gt;
&lt;td&gt;+42.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed Index&lt;/td&gt;
&lt;td&gt;2.82 s&lt;/td&gt;
&lt;td&gt;0.89 s&lt;/td&gt;
&lt;td&gt;-68.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TBT&lt;/td&gt;
&lt;td&gt;828.5 ms&lt;/td&gt;
&lt;td&gt;49 ms&lt;/td&gt;
&lt;td&gt;-94.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTI&lt;/td&gt;
&lt;td&gt;5.91 s&lt;/td&gt;
&lt;td&gt;2.37 s&lt;/td&gt;
&lt;td&gt;-59.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max Potential FID&lt;/td&gt;
&lt;td&gt;421 ms&lt;/td&gt;
&lt;td&gt;99 ms&lt;/td&gt;
&lt;td&gt;-76.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server response&lt;/td&gt;
&lt;td&gt;663 ms&lt;/td&gt;
&lt;td&gt;108 ms&lt;/td&gt;
&lt;td&gt;-83.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total byte weight&lt;/td&gt;
&lt;td&gt;474 KiB&lt;/td&gt;
&lt;td&gt;185 KiB&lt;/td&gt;
&lt;td&gt;-61.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx3btoqrcyi1d74yf37hx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx3btoqrcyi1d74yf37hx.png" alt="Before-and-after comparison of the key Lighthouse metrics" width="799" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The supporting checks passed as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;production build completed successfully;&lt;/li&gt;
&lt;li&gt;all 9 game-engine unit tests passed;&lt;/li&gt;
&lt;li&gt;25 E2E tests passed, with 3 skipped because of device-specific conditions;&lt;/li&gt;
&lt;li&gt;the page Console contained no warnings or errors; and&lt;/li&gt;
&lt;li&gt;all 170 board cells rendered correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why the 98 should not be treated as the whole story
&lt;/h3&gt;

&lt;p&gt;This was not a strict A/B test. The before result came from the deployed site, while the after result came from a local production server. The improvements to TBT, transfer size, and hydration errors are well supported, but server response time is affected by Cloudflare Worker cold and warm states. The LCP values also cannot be compared as if every other variable were identical.&lt;/p&gt;

&lt;p&gt;The increase in LCP from 1.66 s to 2.37 s should not be hidden to make the story look cleaner. The honest interpretation is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2.37 s is still close to the “good” range;&lt;/li&gt;
&lt;li&gt;the different environments do not yet prove that LCP regressed;&lt;/li&gt;
&lt;li&gt;the deployed URL should be measured repeatedly with the same device and network settings; and&lt;/li&gt;
&lt;li&gt;if the production median remains near or above 2.5 s, the LCP element and resource-discovery path deserve a separate investigation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. How to judge whether Codex's performance analysis is trustworthy
&lt;/h2&gt;

&lt;p&gt;Codex can search, run tools, and compare files quickly. Its conclusions still need review. These questions are a useful checklist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this a recommendation or evidence?
&lt;/h3&gt;

&lt;p&gt;“Compress the images” is a general recommendation. “A 512×512 image is displayed at 56×56, wasting roughly 26 KB” is evidence about this page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a complete link between the metric and the cause?
&lt;/h3&gt;

&lt;p&gt;The investigation did not rewrite components merely because React #418 appeared. It confirmed that the boards differed, that React rebuilt 170 nodes, and that the board generator was expensive enough to match the trace.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the change remove work or hide it?
&lt;/h3&gt;

&lt;p&gt;Preventing a second board generation during hydration removes work. Delaying content until after Lighthouse finishes only hides work and usually harms users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are the product invariants written down?
&lt;/h3&gt;

&lt;p&gt;Constraints such as “keep 170 cells” and “do not lose analytics events” prevent an agent from achieving a better score by deleting behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are negative results reported honestly?
&lt;/h3&gt;

&lt;p&gt;A credible report discusses the LCP increase, environment differences, and the risk of not having measured after deployment—not only the improved TBT and total score.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can the conclusion be falsified?
&lt;/h3&gt;

&lt;p&gt;A good root-cause hypothesis suggests a directional experiment. If forcing the same board on both sides removes the mismatch and long tasks, the hypothesis gains support. If it does not, the investigation should continue instead of rationalizing the result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Did real feature tests pass?
&lt;/h3&gt;

&lt;p&gt;Lighthouse covers only part of the product. Interaction, visual output, event delivery, Console errors, and several device conditions need their own checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. A reusable Codex workflow for other projects
&lt;/h2&gt;

&lt;p&gt;The same method can be organized as five independent conversations or five explicit stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Baseline prompt:&lt;/strong&gt; measure and save reports; prohibit edits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnosis prompt:&lt;/strong&gt; connect traces, Console, Network, DOM, and source code into an evidence chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Planning prompt:&lt;/strong&gt; separate the root-cause fix from secondary optimizations, risks, and verification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implementation prompt:&lt;/strong&gt; make causal changes one stage at a time and run targeted tests after each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review prompt:&lt;/strong&gt; measure the production mode again and report every metric, environment difference, and remaining risk.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is more reliable than compressing the request into “optimize everything to 100.” It takes a few more turns, but each technical decision remains traceable and incorrect changes are easier to catch.&lt;/p&gt;

&lt;p&gt;A complete performance task should leave more than a code diff. Keep at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the original and follow-up Lighthouse JSON reports;&lt;/li&gt;
&lt;li&gt;a structured metric summary;&lt;/li&gt;
&lt;li&gt;the root cause and its evidence chain;&lt;/li&gt;
&lt;li&gt;expected versus observed impact for each change;&lt;/li&gt;
&lt;li&gt;automated test results; and&lt;/li&gt;
&lt;li&gt;unverified risks and the conditions for the next optimization.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Conclusion: Codex amplifies judgment rather than replacing it
&lt;/h2&gt;

&lt;p&gt;On the surface, this is a story about moving from Lighthouse 76/79 to 98. The more valuable result was finding a data-ownership bug spanning ISR caching, date seeds, and React hydration.&lt;/p&gt;

&lt;p&gt;If the team had followed generic Lighthouse advice first, it might have compressed images and trimmed JavaScript while leaving the two 400 ms long tasks untouched. Codex's real strength was moving quickly between the browser, reports, and repository to connect evidence that was scattered across those places. The developer's job was to ask the right questions, demand falsifiable evidence, define product constraints, and decide when further optimization was no longer worth the complexity.&lt;/p&gt;

&lt;p&gt;There is no requirement to turn 98 into 100. Main-thread blocking is below 50 ms, the hydration error is gone, image delivery passes its audit, and the game and tests still work. Adding code-splitting and more runtime complexity for the final two points may not improve the user experience.&lt;/p&gt;

&lt;p&gt;The sensible next step is to run mobile Lighthouse at least five times after deployment under the same production conditions. Separate Cloudflare cold and warm results, use the median, and then decide whether another round of LCP work is justified.&lt;/p&gt;

&lt;p&gt;The point of AI-assisted performance work is not to let Codex make every decision for the developer. It is to make each decision faster to investigate, easier to verify, and easier to undo when the evidence says it was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Open-source repository: &lt;a href="https://github.com/weizixiao/fruit-box-game" rel="noopener noreferrer"&gt;fruit-box-game&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Play online: &lt;a href="https://fruitboxgame.com/" rel="noopener noreferrer"&gt;Fruit Box Game&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>lighthouse</category>
      <category>gamedev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>A Practical Guide to Optimizing a Next.js WebGL Game</title>
      <dc:creator>scott winter</dc:creator>
      <pubDate>Wed, 22 Jul 2026 01:09:43 +0000</pubDate>
      <link>https://dev.to/scott_winter_77ced0700c92/from-the-40s-to-97-optimizing-a-nextjs-webgl-game-b01</link>
      <guid>https://dev.to/scott_winter_77ced0700c92/from-the-40s-to-97-optimizing-a-nextjs-webgl-game-b01</guid>
      <description>&lt;p&gt;A Next.js game can look small while still having an expensive initial load.&lt;/p&gt;

&lt;p&gt;The browser may need to hydrate React, initialize WebGL, parse a rendering engine, decode texture atlases, load a tutorial, and request leaderboard data before the player makes a single move.&lt;/p&gt;

&lt;p&gt;That was the problem with &lt;a href="https://colortilesgame.net" rel="noopener noreferrer"&gt;Color Tiles&lt;/a&gt;, a browser game built with Next.js, React, and PixiJS. During optimization, its mobile Lighthouse score improved from a verifiable 59 to 97, while LCP fell from 11.36 seconds to 2.29 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fruitboxgame.com/" rel="noopener noreferrer"&gt;Fruit Box&lt;/a&gt; was also built using the same Next.js, React, and PixiJS stack and the development approach described in this article.&lt;/p&gt;

&lt;p&gt;The score is not the main point of this article. The useful part is the process: decide what the player needs for the first playable screen, then move everything else out of that loading path.&lt;/p&gt;

&lt;p&gt;For a more detailed, metric-by-metric account of the optimization work, read the &lt;a href="https://medium.com/@winterscott999/from-the-40s-to-97-how-we-cut-the-initial-load-of-a-next-js-webgl-game-by-nearly-80-d04e803a27ac" rel="noopener noreferrer"&gt;full Color Tiles performance case study on Medium&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Measure the production loading path
&lt;/h2&gt;

&lt;p&gt;Do not optimize a Next.js development server. Development mode includes compilation, source maps, React checks, and other work that does not represent the deployed application.&lt;/p&gt;

&lt;p&gt;Start with a production build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use Chrome DevTools and Lighthouse with one consistent mobile profile:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run Lighthouse and record LCP, Total Blocking Time, CLS, and transfer size.&lt;/li&gt;
&lt;li&gt;Open the Network panel, disable the cache, reload, and sort requests by size.&lt;/li&gt;
&lt;li&gt;Record a Performance trace and inspect the LCP marker and long tasks.&lt;/li&gt;
&lt;li&gt;Use the Coverage panel to find large JavaScript files with substantial unused code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Classify every initial request into two groups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Needed now:&lt;/strong&gt; the page shell, game controls, a stable board placeholder, and code required for the first interaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Needed later:&lt;/strong&gt; tutorials, leaderboards, below-the-fold content, result dialogs, and optional compatibility code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This classification is more useful than trying to improve the overall Lighthouse score directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Put the game runtime behind a loading boundary
&lt;/h2&gt;

&lt;p&gt;Keep the route and useful page content server-rendered, but isolate browser-only game code in a Client Component. Canvas and WebGL code should not be part of the server-rendering path.&lt;/p&gt;

&lt;p&gt;If the game has a Start or Play action, load the runtime when the player uses it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ComponentType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;GameComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ComponentType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;initialMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GameLoader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initialMode&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;initialMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setGame&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GameComponent&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadGame&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./color-tiles-game&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setGame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ColorTilesGame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Game&lt;/span&gt; &lt;span class="na"&gt;initialMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;initialMode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"game-shell"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loadGame&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Play&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a separate JavaScript chunk and keeps PixiJS, level generation, input handling, and game state out of the initial route bundle.&lt;/p&gt;

&lt;p&gt;Reserve the final game dimensions in the loader so the page does not jump when the chunk arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.game-shell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;760px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the game must start automatically, use &lt;code&gt;next/dynamic&lt;/code&gt; with &lt;code&gt;ssr: false&lt;/code&gt; instead. The important parts are the same: isolate the browser-only module and render a placeholder with stable dimensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Import only the PixiJS modules the game uses
&lt;/h2&gt;

&lt;p&gt;The original implementation imported the complete legacy bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PIXI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pixi.js-legacy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The game only needed the WebGL renderer and a small set of display primitives. Replace the package-wide import with module-level imports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BaseTexture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Renderer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Texture&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pixi/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Container&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pixi/display&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Rectangle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pixi/math&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Sprite&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pixi/sprite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AnimatedSprite&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pixi/sprite-animated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the renderer directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renderer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Renderer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;antialias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;backgroundAlpha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After changing imports, build again and confirm that the old legacy bundle is no longer present in the game chunk. Also test the oldest browser and device class you support. Removing a Canvas fallback is only correct when WebGL is part of your browser support baseline.&lt;/p&gt;

&lt;p&gt;Reducing the bundle saves more than download bytes. Mobile devices also spend less time parsing, compiling, and executing JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Do not mount tutorial media before it is visible
&lt;/h2&gt;

&lt;p&gt;An animated tutorial is useful, but it should not compete with the board during initial loading.&lt;/p&gt;

&lt;p&gt;First, convert animated GIFs to MP4 or WebM. Video usually compresses animation much more efficiently and gives the browser a better decoding path.&lt;/p&gt;

&lt;p&gt;Second, do not render the video element until its modal is open or its section is close to the viewport:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setVisible&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frameRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frameRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;setVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;160px 0px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;frameRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"tutorial-frame"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;video&lt;/span&gt; &lt;span class="na"&gt;autoPlay&lt;/span&gt; &lt;span class="na"&gt;loop&lt;/span&gt; &lt;span class="na"&gt;muted&lt;/span&gt; &lt;span class="na"&gt;playsInline&lt;/span&gt; &lt;span class="na"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;source&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/tutorial-demo.mp4"&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;video&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"tutorial-placeholder"&lt;/span&gt; &lt;span class="na"&gt;aria-hidden&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give the placeholder the same width and aspect ratio as the video. This keeps CLS stable while removing media download and decoding from the first screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Treat page images and game textures differently
&lt;/h2&gt;

&lt;p&gt;Regular React content can use &lt;code&gt;next/image&lt;/code&gt;, responsive &lt;code&gt;sizes&lt;/code&gt;, lazy loading, and an image CDN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/game-preview.webp"&lt;/span&gt;
  &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"A Color Tiles game board"&lt;/span&gt;
  &lt;span class="na"&gt;fill&lt;/span&gt;
  &lt;span class="na"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 760px) 100vw, 600px"&lt;/span&gt;
  &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sizes&lt;/code&gt; value must reflect the rendered CSS width. Without it, a phone may receive an image intended for a desktop layout.&lt;/p&gt;

&lt;p&gt;PixiJS textures and sprite atlases need a different path because the renderer loads their URLs directly. For those assets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Export WebP or AVIF variants where the target browsers and renderer support them.&lt;/li&gt;
&lt;li&gt;Remove transparent padding that is not used by atlas coordinates.&lt;/li&gt;
&lt;li&gt;Keep atlas dimensions and frame rectangles synchronized.&lt;/li&gt;
&lt;li&gt;Put URL generation behind one helper so the CDN hostname or format can change in one place.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageBaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_IMAGE_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://img.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;imageAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;imageBaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a tiny board placeholder during startup. Do not automatically preload the full-resolution background. Preload raises priority, so a large background can delay JavaScript, fonts, or smaller assets that are required sooner.&lt;/p&gt;

&lt;p&gt;Only preload a resource after a Network trace shows that it is discovered too late and is genuinely required for the first playable frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Cache immutable assets with versioned URLs
&lt;/h2&gt;

&lt;p&gt;For local static images, Next.js can send a long cache lifetime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;minimumCacheTTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;31536000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/:path*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.(png|jpg|jpeg|webp|avif|svg)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;public, max-age=31536000, immutable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply equivalent cache rules at the CDN or object storage origin for assets served from another hostname. A header configured in Next.js does not control a separate CDN automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;immutable&lt;/code&gt; is safe only when an asset change also changes its URL. Use content hashes, versioned filenames such as &lt;code&gt;tiles-atlas.v3.webp&lt;/code&gt;, or a version query controlled by the deployment. Never overwrite an immutable URL and expect returning players to receive the new file.&lt;/p&gt;

&lt;p&gt;If a critical asset uses a separate hostname, establish the connection early:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;link&lt;/span&gt; &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"preconnect"&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://img.example.com"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this for an origin needed near the top of the page, not for every third-party host.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Delay API work until the feature needs it
&lt;/h2&gt;

&lt;p&gt;A leaderboard request should not run while the renderer is initializing unless leaderboard data is visible on the first screen.&lt;/p&gt;

&lt;p&gt;Choose the trigger that matches the interface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch after the player starts the game.&lt;/li&gt;
&lt;li&gt;Fetch when the leaderboard approaches the viewport.&lt;/li&gt;
&lt;li&gt;Fetch after a run ends, when the result screen needs the ranking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a below-the-fold panel, &lt;code&gt;IntersectionObserver&lt;/code&gt; is usually enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;refreshRankings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;480px 0px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disconnect the observer after the first request, and guard the fetch function against duplicate calls. The feature remains available, but its network and React work no longer competes with the first playable frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Consider inline CSS only after the large work is gone
&lt;/h2&gt;

&lt;p&gt;Once the JavaScript and media path is under control, a small game page may benefit from Next.js CSS inlining:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;inlineCss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can remove a render-blocking stylesheet request and make the correctly sized game shell available with the HTML.&lt;/p&gt;

&lt;p&gt;It is a finishing optimization, not a first step. It increases the HTML size, repeats styles between pages, and removes the benefit of a separately cached stylesheet. Because the option is experimental, inspect the production output again after every Next.js upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Verify performance and gameplay together
&lt;/h2&gt;

&lt;p&gt;Every loading optimization changes timing, so a faster Lighthouse result is not enough. Run the production build and check that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The initial route does not download the game chunk before its intended trigger.&lt;/li&gt;
&lt;li&gt;Tutorial video and leaderboard requests do not start early.&lt;/li&gt;
&lt;li&gt;The game shell keeps stable dimensions while loading.&lt;/li&gt;
&lt;li&gt;Canvas pixels render instead of remaining blank or transparent.&lt;/li&gt;
&lt;li&gt;Mouse, keyboard, touch, undo, restart, and resize behavior still work.&lt;/li&gt;
&lt;li&gt;Texture URLs return &lt;code&gt;200&lt;/code&gt; and atlas frame mappings remain correct.&lt;/li&gt;
&lt;li&gt;Mobile and desktop layouts do not overflow or overlap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Automate the repeatable checks with Playwright, then use Lighthouse for controlled performance comparisons. Change one related group at a time so a regression can be traced to a specific decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  A useful order of work
&lt;/h2&gt;

&lt;p&gt;For a Next.js browser game, this order usually gives the clearest return:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Separate the server-rendered page from the browser-only game runtime.&lt;/li&gt;
&lt;li&gt;Reduce the rendering-engine bundle.&lt;/li&gt;
&lt;li&gt;Delay tutorials, leaderboards, and below-the-fold content.&lt;/li&gt;
&lt;li&gt;Resize and compress page images and texture atlases.&lt;/li&gt;
&lt;li&gt;Add correct cache rules and connection hints.&lt;/li&gt;
&lt;li&gt;Test smaller finishing changes such as CSS inlining.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The core question is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Must this resource be downloaded, parsed, decoded, or executed before the player can make the first move?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is no, give it a later trigger. That is how a visually small game becomes a genuinely small initial load without removing features from the product.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webperf</category>
      <category>gamedev</category>
      <category>webgl</category>
    </item>
    <item>
      <title>Obvious Online Tools Worth Bookmarking in 2026</title>
      <dc:creator>scott winter</dc:creator>
      <pubDate>Tue, 23 Jun 2026 07:47:48 +0000</pubDate>
      <link>https://dev.to/scott_winter_77ced0700c92/seven-less-obvious-online-tools-worth-bookmarking-in-2026-578m</link>
      <guid>https://dev.to/scott_winter_77ced0700c92/seven-less-obvious-online-tools-worth-bookmarking-in-2026-578m</guid>
      <description>&lt;p&gt;Most tool lists repeat the same names: ChatGPT, Claude, Gemini, Perplexity, Canva, and a few other obvious picks. Those tools are useful, but they are not exactly discoveries anymore.&lt;/p&gt;

&lt;p&gt;This list is a little different. I wanted to collect practical, slightly less obvious tools that can help with writing, meetings, visual thinking, design assets, colors, UI drafts, and quick creative work. Some are AI-first products; some are simple browser tools that remove one small but annoying step from the day.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Napkin AI
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.napkin.ai/" rel="noopener noreferrer"&gt;Napkin AI&lt;/a&gt; turns plain text into visual diagrams, flowcharts, and business-style graphics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paste notes, bullets, or a short explanation, then generate visuals from the text.&lt;/li&gt;
&lt;li&gt;Useful for blog posts, decks, internal docs, product explanations, and strategy notes.&lt;/li&gt;
&lt;li&gt;Better for communicating an idea visually than starting from a blank canvas.&lt;/li&gt;
&lt;li&gt;Handy when you need a diagram quickly but do not want to open a full design tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your written explanation feels too dense, Napkin AI can help turn it into something easier to scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Granola
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.granola.ai/" rel="noopener noreferrer"&gt;Granola&lt;/a&gt; is an AI meeting notepad for people who spend too much time in calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Captures meeting notes and turns them into cleaner summaries.&lt;/li&gt;
&lt;li&gt;Helps extract decisions, action items, and useful context after a conversation.&lt;/li&gt;
&lt;li&gt;Good for founders, product managers, recruiters, sales teams, and anyone doing back-to-back calls.&lt;/li&gt;
&lt;li&gt;Feels more like a private working notebook than a big meeting bot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One note: meeting tools can touch sensitive information, so check your sharing and privacy settings before using them for confidential calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Font Generator Online
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://fontgeneratoronline.net/" rel="noopener noreferrer"&gt;Font Generator Online&lt;/a&gt; is a free fancy text generator for making stylized Unicode text you can copy and paste anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Includes 290+ font styles, such as cursive, gothic, bold, tiny, calligraphy, cute, bubble, 3D, Instagram, Discord, and more.&lt;/li&gt;
&lt;li&gt;Works directly in the browser: type your text, preview styles, then copy the result.&lt;/li&gt;
&lt;li&gt;Useful for social bios, usernames, captions, quick branding experiments, and profile decoration.&lt;/li&gt;
&lt;li&gt;No design software or account setup is needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is a small tool, but it solves a common problem fast: making plain text look more distinctive.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Recraft
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.recraft.ai/" rel="noopener noreferrer"&gt;Recraft&lt;/a&gt; is an AI design platform for generating and editing images, vectors, icons, mockups, and brand assets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generates images, illustrations, logos, icons, and vector-style assets.&lt;/li&gt;
&lt;li&gt;Useful when you need editable visual elements instead of just a flat AI image.&lt;/li&gt;
&lt;li&gt;Includes tools like background removal, upscaling, erasing, mockups, and image editing.&lt;/li&gt;
&lt;li&gt;A good fit for designers, indie makers, marketers, and product teams.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you often need small visual assets for landing pages, ads, or product mockups, Recraft is worth testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. DuckColor
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://duckcolor.com/" rel="noopener noreferrer"&gt;DuckColor&lt;/a&gt; is an online AI color palette generator for websites, UI design, branding, and creative projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate palettes from a prompt when you know the mood but not the exact colors.&lt;/li&gt;
&lt;li&gt;Copy HEX values quickly for CSS, mockups, and design systems.&lt;/li&gt;
&lt;li&gt;Includes image color picking and a palette workspace for exploring combinations.&lt;/li&gt;
&lt;li&gt;Helps with contrast checks, which matters when colors need to be readable in real UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is especially useful for developers who can build the interface but do not want to guess their way through color choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Uizard
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://uizard.io/" rel="noopener noreferrer"&gt;Uizard&lt;/a&gt; is an AI-assisted UI design tool for creating app, website, and desktop software mockups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Helps turn ideas, screenshots, or rough sketches into editable UI concepts.&lt;/li&gt;
&lt;li&gt;Useful for quick prototypes before opening heavier design tools.&lt;/li&gt;
&lt;li&gt;Good for product managers, founders, and developers who need to communicate a screen idea.&lt;/li&gt;
&lt;li&gt;Works well for early-stage layout exploration, not just polished final design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Uizard is useful when the goal is to make an idea visible quickly enough for feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Krea
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.krea.ai/" rel="noopener noreferrer"&gt;Krea&lt;/a&gt; is a creative AI suite for generating, enhancing, and editing images, videos, and 3D assets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supports image generation, video generation, enhancement, editing, and 3D-style workflows.&lt;/li&gt;
&lt;li&gt;Useful for experimenting with visual directions before committing to one style.&lt;/li&gt;
&lt;li&gt;Good for creators who want to iterate on visual concepts quickly.&lt;/li&gt;
&lt;li&gt;Works well for moodboards, campaign ideas, thumbnails, posters, and creative references.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you like exploring visual ideas through fast iteration, Krea gives you a broader playground than a single-purpose image generator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The best tools are often not the biggest ones. They are the ones that remove a specific point of friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Napkin AI helps turn text into visuals.&lt;/li&gt;
&lt;li&gt;Granola helps clean up meeting notes.&lt;/li&gt;
&lt;li&gt;Font Generator Online helps create stylized text quickly.&lt;/li&gt;
&lt;li&gt;Recraft helps generate and edit design assets.&lt;/li&gt;
&lt;li&gt;DuckColor helps choose better palettes.&lt;/li&gt;
&lt;li&gt;Uizard helps sketch UI ideas fast.&lt;/li&gt;
&lt;li&gt;Krea helps explore creative visual directions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Used together, they make a practical toolkit for creators, developers, indie makers, and small teams who want to move faster without adding too much process.&lt;/p&gt;

</description>
      <category>website</category>
      <category>software</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
