perf: optimize portfolio runtime delivery
Replace the blocking asset loader with progressive rendering, defer expensive media and Three.js work, and reduce hydration and animation costs. Move deployment from a static Nginx export to the standalone Next.js runtime with optimized dynamic images.
This commit is contained in:
@@ -14,10 +14,21 @@ test('the isometric footer preserves the real contact destinations and email', a
|
||||
|
||||
test('the footer renders the IsometricFooter scene instead of preloading PNG artwork', async () => {
|
||||
const footerSection = await readFile('src/components/FooterSection.tsx', 'utf8');
|
||||
const loader = await readFile('src/components/SiteLoader.tsx', 'utf8');
|
||||
|
||||
assert.match(footerSection, /<IsometricFooter locale=\{locale\}\s*\/>/);
|
||||
assert.doesNotMatch(footerSection, /next\/image/);
|
||||
assert.doesNotMatch(footerSection, /footer-background.*\.png/);
|
||||
assert.doesNotMatch(loader, /footer-background.*\.png/);
|
||||
});
|
||||
|
||||
test('the footer and large SVG stay server-rendered with delegated interaction', async () => {
|
||||
const [footer, scene, interaction] = await Promise.all([
|
||||
readFile('src/components/IsometricFooter/IsometricFooter.tsx', 'utf8'),
|
||||
readFile('src/components/IsometricFooter/IsometricScene.tsx', 'utf8'),
|
||||
readFile('src/components/IsometricFooter/FooterInteraction.tsx', 'utf8'),
|
||||
]);
|
||||
|
||||
assert.doesNotMatch(footer, /^['"]use client['"]/);
|
||||
assert.doesNotMatch(scene, /^['"]use client['"]/);
|
||||
assert.match(interaction, /^['"]use client['"]/);
|
||||
assert.match(footer, /data-contact-id=/);
|
||||
});
|
||||
|
||||
48
tests/performance-architecture.test.mjs
Normal file
48
tests/performance-architecture.test.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
test('Next runs as a standalone server with the image optimizer enabled', async () => {
|
||||
const [config, packageJson] = await Promise.all([
|
||||
readFile('next.config.mjs', 'utf8'),
|
||||
readFile('package.json', 'utf8').then(JSON.parse),
|
||||
]);
|
||||
|
||||
assert.match(config, /output:\s*['"]standalone['"]/);
|
||||
assert.doesNotMatch(config, /unoptimized:\s*true/);
|
||||
assert.match(config, /formats:\s*\[['"]image\/webp['"]\]/);
|
||||
assert.match(config, /qualities:\s*\[60,\s*75,\s*85\]/);
|
||||
assert.ok(packageJson.dependencies.sharp, 'sharp must be a direct production dependency');
|
||||
});
|
||||
|
||||
test('the blocking loader and eager preload manifest are removed', async () => {
|
||||
const [root, page] = await Promise.all([
|
||||
readFile('src/app/SiteRoot.tsx', 'utf8'),
|
||||
readFile('src/app/PortfolioPage.tsx', 'utf8'),
|
||||
]);
|
||||
|
||||
assert.doesNotMatch(root, /siteLoading|site-loader/);
|
||||
assert.doesNotMatch(page, /SiteLoader/);
|
||||
});
|
||||
|
||||
test('Three.js is only imported by the deferred scene implementation', async () => {
|
||||
const [boundary, scene] = await Promise.all([
|
||||
readFile('src/components/scenes/DeferredAboutRiverScene.tsx', 'utf8'),
|
||||
readFile('src/components/scenes/AboutRiverScene.tsx', 'utf8'),
|
||||
]);
|
||||
|
||||
assert.doesNotMatch(boundary, /from ['"]three['"]/);
|
||||
assert.match(boundary, /import\(['"].\/AboutRiverScene['"]\)/);
|
||||
assert.match(boundary, /rootMargin:\s*['"]600px/);
|
||||
assert.match(scene, /from ['"]three['"]/);
|
||||
});
|
||||
|
||||
test('local font loading replaces remote CSS imports and TTF delivery', async () => {
|
||||
const [root, css] = await Promise.all([
|
||||
readFile('src/app/SiteRoot.tsx', 'utf8'),
|
||||
readFile('src/styles/globals.css', 'utf8'),
|
||||
]);
|
||||
|
||||
assert.match(root, /localFont/);
|
||||
assert.doesNotMatch(css, /fonts\.googleapis\.com|@font-face|IRANYekanXVF\.ttf/);
|
||||
});
|
||||
91
tests/runtime-server.test.mjs
Normal file
91
tests/runtime-server.test.mjs
Normal file
@@ -0,0 +1,91 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { access, cp, readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
const projectRoot = path.resolve('.');
|
||||
const standaloneRoot = path.join(projectRoot, '.next', 'standalone');
|
||||
const port = 43217;
|
||||
const origin = `http://127.0.0.1:${port}`;
|
||||
|
||||
async function waitForServer(process, getLogs) {
|
||||
const deadline = Date.now() + 60_000;
|
||||
while (Date.now() < deadline) {
|
||||
if (process.exitCode !== null) throw new Error('standalone server exited early');
|
||||
try {
|
||||
const response = await fetch(origin, { signal: AbortSignal.timeout(10_000) });
|
||||
if (response.ok) return;
|
||||
} catch {}
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
}
|
||||
throw new Error(`standalone server did not become ready\n${getLogs()}`);
|
||||
}
|
||||
|
||||
test('the standalone server renders both locales and optimizes local images', async (t) => {
|
||||
await access(path.join(standaloneRoot, 'server.js'));
|
||||
await cp(path.join(projectRoot, 'public'), path.join(standaloneRoot, 'public'), { recursive: true });
|
||||
await cp(
|
||||
path.join(projectRoot, '.next', 'static'),
|
||||
path.join(standaloneRoot, '.next', 'static'),
|
||||
{ recursive: true },
|
||||
);
|
||||
|
||||
const server = spawn(process.execPath, ['server.js'], {
|
||||
cwd: standaloneRoot,
|
||||
env: { ...process.env, HOSTNAME: '127.0.0.1', PORT: String(port) },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let serverLogs = '';
|
||||
server.stdout.on('data', (chunk) => { serverLogs += chunk; });
|
||||
server.stderr.on('data', (chunk) => { serverLogs += chunk; });
|
||||
t.after(() => server.kill());
|
||||
await waitForServer(server, () => serverLogs);
|
||||
|
||||
const [englishResponse, persianResponse] = await Promise.all([
|
||||
fetch(`${origin}/`),
|
||||
fetch(`${origin}/fa`),
|
||||
]);
|
||||
assert.equal(englishResponse.status, 200);
|
||||
assert.equal(persianResponse.status, 200);
|
||||
|
||||
const [englishHtml, persianHtml] = await Promise.all([
|
||||
englishResponse.text(),
|
||||
persianResponse.text(),
|
||||
]);
|
||||
assert.match(englishHtml, /<html[^>]*lang="en"[^>]*dir="ltr"/i);
|
||||
assert.match(englishHtml, /href="\/fa"/i);
|
||||
assert.match(englishHtml, /hreflang="fa"/i);
|
||||
assert.match(englishHtml, />Software Engineer</);
|
||||
assert.doesNotMatch(englishHtml, /id="site-loader"/);
|
||||
assert.match(persianHtml, /<html[^>]*lang="fa"[^>]*dir="rtl"/i);
|
||||
assert.match(persianHtml, /سلام، من امیرحسین خلیلی هستم/);
|
||||
|
||||
const staticAsset = englishHtml.match(/(?:src|href)="(\/_next\/static\/[^"?#]+)/)?.[1];
|
||||
assert.ok(staticAsset, 'expected a built static asset');
|
||||
assert.equal((await fetch(`${origin}${staticAsset}`)).status, 200);
|
||||
|
||||
const optimized = await fetch(
|
||||
`${origin}/_next/image?url=%2Fimages%2Favatar.png&w=256&q=75`,
|
||||
{ headers: { Accept: 'image/webp' } },
|
||||
);
|
||||
assert.equal(optimized.status, 200);
|
||||
assert.equal(optimized.headers.get('content-type'), 'image/webp');
|
||||
assert.match(optimized.headers.get('cache-control') ?? '', /max-age=/);
|
||||
});
|
||||
|
||||
test('Persian source copy uses standard characters and punctuation', async () => {
|
||||
const localizedFiles = [
|
||||
'src/i18n/content.ts',
|
||||
'src/data/projects.ts',
|
||||
'src/data/experience.ts',
|
||||
'src/data/education.ts',
|
||||
'src/data/milestones.ts',
|
||||
];
|
||||
const source = (
|
||||
await Promise.all(localizedFiles.map((file) => readFile(file, 'utf8')))
|
||||
).join('\n');
|
||||
|
||||
assert.doesNotMatch(source, /[يك]/, 'use Persian ی and ک instead of Arabic forms');
|
||||
assert.doesNotMatch(source, /[—–]/, 'use Persian punctuation instead of em/en dashes');
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { access, readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
const exportRoot = path.resolve('out');
|
||||
|
||||
test('the production build exports a usable home page', async () => {
|
||||
const indexPath = path.join(exportRoot, 'index.html');
|
||||
const html = await readFile(indexPath, 'utf8');
|
||||
|
||||
assert.match(html, /<!DOCTYPE html>/i);
|
||||
assert.match(html, /<html[^>]*lang=/i);
|
||||
|
||||
const assetPaths = [
|
||||
...html.matchAll(/(?:src|href)="(\/_next\/static\/[^"?#]+)/g),
|
||||
].map((match) => match[1]);
|
||||
|
||||
assert.ok(assetPaths.length > 0, 'expected the home page to reference built assets');
|
||||
|
||||
await Promise.all(
|
||||
[...new Set(assetPaths)].map((assetPath) =>
|
||||
access(path.join(exportRoot, assetPath.replace(/^\//, ''))),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('the production build exports reciprocal English and Persian pages', async () => {
|
||||
const englishHtml = await readFile(path.join(exportRoot, 'index.html'), 'utf8');
|
||||
const persianHtml = await readFile(path.join(exportRoot, 'fa.html'), 'utf8');
|
||||
|
||||
assert.match(englishHtml, /<html[^>]*lang="en"[^>]*dir="ltr"/i);
|
||||
assert.match(englishHtml, /href="\/fa"[^>]*hreflang="fa"/i);
|
||||
assert.match(englishHtml, />Software Engineer</);
|
||||
assert.match(englishHtml, />Quera</);
|
||||
assert.doesNotMatch(englishHtml, />مهندس نرمافزار</);
|
||||
assert.doesNotMatch(englishHtml, />کوئرا</);
|
||||
|
||||
assert.match(persianHtml, /<html[^>]*lang="fa"[^>]*dir="rtl"/i);
|
||||
assert.match(persianHtml, /href="\/"[^>]*hreflang="en"/i);
|
||||
assert.match(persianHtml, /سلام، من امیرحسین خلیلی هستم/);
|
||||
assert.match(persianHtml, /از پیکسل تا محصول/);
|
||||
assert.match(persianHtml, /بهمن ۱۴۰۴ تا امروز/);
|
||||
assert.match(persianHtml, /آبان ۱۴۰۳ تا امروز/);
|
||||
});
|
||||
|
||||
test('Persian source copy uses standard characters and punctuation', async () => {
|
||||
const localizedFiles = [
|
||||
'src/i18n/content.ts',
|
||||
'src/data/projects.ts',
|
||||
'src/data/experience.ts',
|
||||
'src/data/education.ts',
|
||||
'src/data/milestones.ts',
|
||||
];
|
||||
const source = (
|
||||
await Promise.all(localizedFiles.map((file) => readFile(file, 'utf8')))
|
||||
).join('\n');
|
||||
|
||||
assert.doesNotMatch(source, /[يك]/, 'use Persian ی and ک instead of Arabic forms');
|
||||
assert.doesNotMatch(source, /[—–]/, 'use Persian punctuation instead of em/en dashes');
|
||||
});
|
||||
Reference in New Issue
Block a user