pseo-performance
Skill54 starsChanged 15 days agoMITView on GitHub
Optimize a programmatic SEO application for Core Web Vitals, build performance, and scalability at 1000+ pages, including static generation, incremental regeneration, bundle optimization, and caching. Use when builds are slow, pages are large, Core Web Vitals scores are poor, or when scaling to many pages causes performance issues.
- Reads credentials
- Sends data out
1 other project uses a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- karrolcia/hippocampusedited, first seen Mar 2, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 1
- karrolcia/hippocampus★ 1376% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository17 other files it gives its agents.
- gscSkill
- pseo-auditSkill
- pseo-dataSkill
- pseo-discoverySkill
- pseo-linkingSkill
- pseo-llm-visibilitySkill
- pseo-metadataSkill
- pseo-orchestrateSkill
- pseo-quality-guardSkill
- pseo-scaleSkill
- pseo-schemaSkill
- pseo-templatesSkill
- seo-content-refreshSkill
- seo-i18nSkill
- seo-log-analysisSkill
- seo-migrationSkill
- seo-regression-guardSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
26 rules. About 0 are common, 18 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈18
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 25
Incremental where needed: Use ISR for pages that change frequently
- Distinctive: line 27
Image optimization: All images processed, sized, and lazy-loaded
- Very specific: line 80
If you can't list any real params, return one placeholder param and call notFound() for it (the documented workaround).
- Distinctive: line 87
Prefer a fallback that keeps the HTML meaningful for crawlers, not an empty shell
- Distinctive: line 103
Limit build concurrency: If the build server has limited memory, configure worker limits
- Distinctive: line 104
Incremental builds: Use ISR to avoid rebuilding all pages on every deploy
- Distinctive: line 127
Search Console Core Web Vitals report: groups URLs by similar pages.
- Very specific: line 151
Use next/image or equivalent for all images (automatic sizing, WebP, lazy loading)
- Distinctive: line 159
Never inject content above the fold after initial render
- Very specific: line 160
Use font-display: swap with size-adjusted fallback fonts
- Distinctive: line 165
Avoid long tasks in event handlers
- Very specific: line 166
Use React.lazy() for below-fold interactive components
- Distinctive: line 170
Analyze the bundle: Run the framework's bundle analyzer
- Distinctive: line 171
Tree-shake unused code: Ensure imports are specific, not barrel imports
- Distinctive: line 187
Use file-system or in-memory caching for computed values
- Distinctive: line 192
Use ISR revalidation to keep cached pages fresh
- Distinctive: line 198
Use stale-while-revalidate for non-critical freshness
- Distinctive: line 202
Use the framework's image component (next/image, etc.)
- Distinctive: line 207
Use a CDN image optimizer for dynamic images
- Very specific: line 246
Use sharp's built-in concurrency limiter: sharp.concurrency(1)
- Very specific: line 247
If using next/image with remote images, limit simultaneous optimizations
- Very specific: line 263
Don't emit changefreq or priority (Google ignores them).
- Distinctive: line 294
Search Console: indexing status, manual actions, coverage issues ("Discovered - currently not indexed" trends)
- Distinctive: line 303
Use the Indexing API (JobPosting and BroadcastEvent only) or IndexNow (does not include Google) as a Google submission shortcut
- Distinctive: line 304
Create all pages and then remove them quickly if they don't rank (low-quality churn).
- Very specific: line 310
Use font-display: swap or optional
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
pSEO Performance Optimization
Optimize the application for fast builds, excellent Core Web Vitals, and reliable performance at 1000+ page scale.
Scope parameter
| Value | Sections |
build | 1, 2, 7, 8 |
cwv | 3, 6, 9 |
bundle | 4 |
caching | 5 |
all | everything |
Core Principles
- Static first: Pre-render as many pages as possible at build time
- Incremental where needed: Use ISR for pages that change frequently
- Minimal JavaScript: Pages should be functional with minimal client-side JS
- Image optimization: All images processed, sized, and lazy-loaded
- Cache aggressively: Cache data fetches, API calls, and rendered output
Optimization Areas
1. Static Generation Strategy
Choose the right rendering strategy for scale:
| Page Count | Strategy | Implementation |
| < 500 | Full SSG | Generate all pages at build time |
| 500-5000 | SSG + ISR | Generate high-traffic pages at build, ISR for rest |
| 5000+ | ISR + on-demand | Generate on first request, revalidate periodically |
Next.js App Router (without Cache Components, Next.js 15 style and still valid in 16):
// Generate most important pages at build time
export async function generateStaticParams() {
const topPages = await getTopPages(500);
return topPages.map((p) => ({ slug: p.slug }));
}
// ISR for the rest
export const revalidate = 86400; // 24 hoursNext.js 16.4.x (>= 16.4.1 for security fixes) with Cache Components (cacheComponents: true, enabled by default in create-next-app from 16.4, planned default in Next.js 17). Caching is opt-in via "use cache", and dynamic is the default:
import { cacheLife, cacheTag } from "next/cache";
import { notFound } from "next/navigation";
export async function generateStaticParams() {
// With Cache Components this MUST return at least one param.
// An empty array is a build error.
const topPages = await getTopPages(500);
return topPages.map((p) => ({ slug: p.slug }));
}
async function getPage(slug: string) {
"use cache";
cacheLife("days");
cacheTag(`page:${slug}`);
return fetchSinglePage(slug);
}
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params; // params is async-only in Next.js 16
const page = await getPage(slug);
if (!page) notFound(); // see "Status codes with streaming" below
return <PageView page={page} />;
}- If you can't list any real params, return one placeholder param and call
notFound()for it (the documented workaround). ensureStatic(requirescacheComponents): only"navigation"adds build validation (the build fails if dynamic content sneaks in), and it requiresgenerateStaticParamsto return every param."shell"and"prefetch"add no build validation. With seeded params and on-demand rest, do not claimensureStaticvalidates anything. Details: https://github.com/lisbeth718/pseo-skills/blob/main/docs/research-2026-10.md (section 11.4).- Invalidate with
revalidateTag(tag, profile)(a cache-life profile is required in Next.js 16),updateTag(tag)for read-your-writes in Server Actions, orrefresh(). - Turbopack is the default bundler in Next.js 16.
next buildno longer runs lint, so run your linter in a pre-push or post-build step. - Next.js 16.3 added a better ISR path: URLs omitted from build-time prerendering can serve an instant loading shell to the first visitor. Mind the status-code caveat below.
Fallback handling:
- Prefer a fallback that keeps the HTML meaningful for crawlers, not an empty shell
- Return
notFound()for genuinely invalid slugs - Without Cache Components,
dynamicParams = trueallows ISR for pages not ingenerateStaticParams, anddynamicParams = falsemakes unlisted params 404
Status codes with streaming: a streamed response starts with HTTP 200. If notFound() runs after streaming has begun (inside a Suspense boundary, or after loading.tsx renders), Next.js returns 200 with an injected <meta name="robots" content="noindex">, and Google may report a soft 404. For "no data for this combination" pages, check existence before any Suspense fallback renders, or return the 404 from proxy.ts (the Next.js 16 replacement for middleware.ts).
Metadata placement: generateMetadata output can be streamed into <body> for JS-executing bots. Google's canonical docs specify <head>. Keep canonical and robots metadata static or use cache so it lands in the head shell, or force head placement with htmlLimitedBots: /.*/. (This is a recommendation, not a Google statement.)
HTML size: Googlebot for Search fetches only the first 2MB of uncompressed HTML (64MB for PDF), and each CSS/JS file has its own limit. Inline JSON, large RSC payloads and giant tables can push content past the cutoff and get it truncated. Keep key content, canonical, meta and links near the top, and check the uncompressed size of your heaviest template.
2. Build Performance
For builds with many pages:
- Fetch index-tier data once; full page data per page. Parallelize within a page, not by loading the whole corpus
- Memoize data access:
React.cache()only dedupes within one render/request. For build-wide reuse use a module-level memoized promise, or"use cache" - Limit build concurrency: If the build server has limited memory, configure worker limits
- Incremental builds: Use ISR to avoid rebuilding all pages on every deploy
- Monitor build time: Track build duration and set alerts for regressions
// Memoize INDEX-TIER data at module level (lightweight: slug, title, category)
// NEVER cache full page content this way - see section 7 Memory Management
let indexPromise: Promise<PageIndex[]> | undefined;
export function getAllIndexData(): Promise<PageIndex[]> {
// PageIndex[] (~1KB per page) - safe to hold in memory. One fetch per process.
indexPromise ??= fetchAllIndexDataFromSource();
return indexPromise;
}
// With Cache Components, an async function with "use cache" + cacheLife() works too.3. Core Web Vitals
Thresholds are assessed on field data at the 75th percentile, split by mobile and desktop. They are unchanged: LCP <= 2.5s, INP <= 200ms, CLS <= 0.1. Lab tools (Lighthouse, PageSpeed lab run) help debug but do not decide; do not set lab targets such as "3s on 3G".
Experimental soft-navigation CWV (2026-10-13): CrUX now exposes experimental_largest_contentful_paint_with_softnavs, experimental_interaction_to_next_paint_with_softnavs and experimental_cumulative_layout_shift_with_softnavs per URL (BigQuery, CrUX API, CrUX History API). Stable CWV and Search Console are unchanged; Google aims to graduate them in May 2027, after which Search Console switches to them. Client navigations between template pages will then count per URL. The web-vitals library v6 can track soft navigations in RUM.
Measure field data per template:
- Search Console Core Web Vitals report: groups URLs by similar pages; use it to find which template fails.
- CrUX API for a URL or origin (only URLs/origins with enough traffic return data; 404 means no data):
const res = await fetch(
`https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${process.env.CRUX_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://example.com/category/sample-page", // or { origin: "https://example.com" }
formFactor: "PHONE",
metrics: ["largest_contentful_paint", "interaction_to_next_paint", "cumulative_layout_shift"],
}),
}
);
const { record } = await res.json();
const lcpP75 = record.metrics.largest_contentful_paint.percentiles.p75;- CrUX History API (
https://chromeuxreport.googleapis.com/v1/records:queryHistoryRecord, same body) returns weekly time series; use it to confirm a template fix moved the 75th percentile. - Sample 5-10 URLs per template; a template with no URL-level data falls back to origin data.
Largest Contentful Paint (LCP) < 2.5s:
- Use
next/imageor equivalent for all images (automatic sizing, WebP, lazy loading) - Preload the LCP image with the
preloadprop (Next 16;priorityis deprecated since v16.0.0, and the docs recommendloading="eager"orfetchPriority="high"in most cases) or<link rel="preload"> - Minimize render-blocking CSS; inline critical CSS
- Serve from a CDN with edge caching
Cumulative Layout Shift (CLS) < 0.1:
- Set explicit
widthandheighton all images and embeds - Reserve space for dynamic content with CSS (min-height, aspect-ratio)
- Never inject content above the fold after initial render
- Use
font-display: swapwith size-adjusted fallback fonts
Interaction to Next Paint (INP) < 200ms:
- Minimize client-side JavaScript
- Defer non-critical scripts
- Avoid long tasks in event handlers
- Use
React.lazy()for below-fold interactive components
4. Bundle Optimization
- Analyze the bundle: Run the framework's bundle analyzer
- Tree-shake unused code: Ensure imports are specific, not barrel imports
- Code split by route: Each page route should have its own chunk
- Externalize large dependencies: Move heavy libraries to CDN or dynamic imports
- Remove unused dependencies: Audit
package.jsonfor dead dependencies
# Next.js bundle analysis
# @next/bundle-analyzer is webpack-only: Next 16 builds with Turbopack by default,
# so run it with the webpack flag (or use Turbopack's own analyzer if your version ships one)
ANALYZE=true next build --webpack5. Caching Strategy
Build-time caching:
- Cache data source responses during build
- Use file-system or in-memory caching for computed values
- Cache static assets with immutable headers
Runtime caching:
- Set
Cache-Controlheaders for static pages (e.g.,s-maxage=86400, stale-while-revalidate) - Use ISR revalidation to keep cached pages fresh
- Cache API responses with appropriate TTLs
CDN caching:
- Deploy behind a CDN (Vercel, Cloudflare, etc.)
- Configure cache keys to avoid unnecessary invalidation
- Use stale-while-revalidate for non-critical freshness
6. Image Optimization
- Use the framework's image component (next/image, etc.)
- Serve images in WebP/AVIF format
- Implement responsive
srcSetfor different viewports - Lazy load below-fold images
- Set explicit dimensions to prevent layout shift
- Use a CDN image optimizer for dynamic images
7. Memory Management at Scale
Node.js default heap is ~1.5GB. At 1000+ pages with rich content, builds will OOM without explicit memory management.
Increase Node.js heap when needed:
# In build script or CI
NODE_OPTIONS="--max-old-space-size=4096" next buildLimit build worker concurrency:
// next.config.js
module.exports = {
experimental: {
workerThreads: true,
cpus: 2, // limit parallel workers to reduce total memory
},
};Split data loading into light and heavy tiers:
- Index data (slug, title, category, lastModified) - safe to hold all in memory. ~1KB per page = ~1MB for 1000 pages.
- Full page data (body content, FAQs, images) - load per-page, never cache the full set. ~50-500KB per page = 50MB-500MB for 1000 pages. This will OOM at scale if cached.
// GOOD: Load full data per page
export async function getPageData(slug: string) {
return fetchSinglePage(slug); // loads ~100KB, GC'd after render
}
// BAD: Cache all full data in memory
const ALL_DATA = await fetchAllPages(); // 500MB+ in memory for entire buildImage processing concurrency:
- Process images in batches, not all at once
- Use
sharp's built-in concurrency limiter:sharp.concurrency(1) - If using
next/imagewith remote images, limit simultaneous optimizations
ISR cache eviction:
- Next.js ISR caches rendered pages in memory (or disk). At 10,000+ pages, configure:
- Disk-based cache (default in recent Next.js versions)
- Set top-level
cacheMaxMemorySize: 0to disable the in-memory cache entirely if memory-constrained
// next.config.js module.exports = { cacheMaxMemorySize: 0, // rely on disk cache only };
Sitemap generation:
- Google's limits are 50,000 URLs or 50MB uncompressed per sitemap file. Split into chunks of 10,000-50,000 URLs and stream the XML rather than building the full array in memory
- Next.js
generateSitemapsdoes not create a sitemap index. Write the index yourself (see pseo-linking section 6) - Don't emit
changefreqorpriority(Google ignores them). Setlastmodonly from a real content-modified timestamp, nevernew Date()at build time
Monitor memory during builds:
# Track peak memory usage
/usr/bin/time -v next build # Linux
/usr/bin/time -l next build # macOS8. Rollout Strategy and Publication Velocity
Google documents no publication-speed signal, and no primary source says a fast publish rate is penalized by itself. The risk is quality: Google's spam policies target many pages created mainly to rank without adding value (scaled content abuse), and a large batch of thin pages hurts the site-wide quality presumption ("individual pages (including new pages) match the overall quality of other pages on the domain"). Roll out gradually for operational reasons: you can catch template bugs, check indexing, and watch quality before committing the whole corpus. Treat the table below as operational advice, not a Google rule.
Rollout strategy for new pSEO pages:
| Page Count | Rollout Strategy |
| < 100 | Deploy all at once, low risk |
| 100-1K | Deploy over 1-4 weeks in batches of 50-200 |
| 1K-10K | Deploy over 4-8 weeks in batches; validate indexing and ranking quality per batch |
| 10K+ | ISR-only (don't pre-build). Submit sitemaps in category waves. Use data sufficiency gating to exclude thin pages. See pseo-scale for the full strategy |
These tiers are an operational heuristic, shared with pseo-scale, not a Google rule.
Implementation approaches:
- ISR with gradual seeding: Generate pages on-demand via ISR but add URLs to the sitemap in batches
- Feature flag by category: Launch one category at a time, monitor impact before launching the next
- Draft/published status: Mark pages as draft in the data layer, publish in batches by flipping status
Monitor between batches:
- Search Console: indexing status, manual actions, coverage issues ("Discovered - currently not indexed" trends)
- Organic traffic: are existing pages maintaining rankings?
- Crawl stats: is Googlebot crawling the new pages?
- Core Web Vitals: are new pages performing well?
- Spam updates are frequent and short (four in 2026 so far); check for movement after each
Do NOT:
- Publish thousands of pages in a single deploy without a quality check and a way to pull them back
- Bulk-submit URLs with Request Indexing or the URL Inspection API. Request Indexing is quota-limited (reserve it for your most important URLs), and the URL Inspection API allows 2,000 requests/day and 600/min per site
- Use the Indexing API (JobPosting and BroadcastEvent only) or IndexNow (does not include Google) as a Google submission shortcut
- Create all pages and then remove them quickly if they don't rank (low-quality churn). If you must remove them, return 404/410, not soft 404s
9. Font Optimization
- Self-host fonts (or use
next/font) - Subset fonts to required character sets
- Use
font-display: swaporoptional - Preload the primary font file
Performance Checklist
- Field data (CrUX / Search Console CWV report) checked per template; no template in the "poor" group
- LCP <= 2.5s, INP <= 200ms, CLS <= 0.1 at the 75th percentile (mobile and desktop)
- Build completes in reasonable time at current page count
- Build peak memory stays within server limits (check with
/usr/bin/time) - No full-dataset loading in memory (two-tier data pattern used)
- Bundle size per page < 200KB JS (compressed)
- Images use next-gen formats (WebP/AVIF)
- Cache-Control headers are set for all static assets
- No unused JavaScript in production bundle
- Rollout plan exists for 100+ new pages (batches with quality and indexing checks)
- Uncompressed HTML of the heaviest template is well under Googlebot's 2MB limit
- "No data" pages return a real 404 before streaming starts (not 200 + noindex)
- Search Console monitoring is configured between deployment batches
Verify
Build, start the app, and run seo-regression-guard against localhost (npx -y tsx <seo-regression-guard-dir>/scripts/seo-check.ts --site http://localhost:3000 ...). Zero errors for touched templates, then confirm the heaviest template's uncompressed HTML size.
Relationship to Other Skills
- Optimizes: pseo-templates (rendering strategy), pseo-data (fetch performance)
- Independent of: pseo-metadata, pseo-schema, pseo-linking (these are typically lightweight)
- Extended by: pseo-scale (CDN/edge architecture, build strategy at 100K, cache warm-up, crawl budget, Vercel cost optimization)
- Validated by: Lighthouse, WebPageTest, or framework-specific performance tools
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

