feat(frontend): replace lazy loading with zibll lazysizes

This commit is contained in:
2026-08-18 01:33:38 +08:00
parent 02571eecd6
commit 435ee1daa7
78 changed files with 1515 additions and 909 deletions
@@ -0,0 +1,138 @@
'use strict';
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const lazyDefaults = {
lazy_posts_thumb: true,
lazy_avatar: true,
lazy_posts_content: false,
lazy_comment: true,
lazy_private: true,
lazy_sider: true,
lazy_cover: true,
lazy_other: true,
lazy_animation: 'blur',
};
function phpHarness(body, values = {}, userAgent = 'Mozilla/5.0') {
const helper = path.join(root, 'inc/helpers/template-tags/pagination-permissions.php');
const code = `
define('ABSPATH', __DIR__);
$_SERVER['HTTP_USER_AGENT'] = ${JSON.stringify(userAgent)};
function add_filter() {}
function add_action() {}
function is_admin() { return false; }
function wp_doing_ajax() { return false; }
function sanitize_key($value) { return preg_replace('/[^a-z0-9_\\-]/', '', strtolower($value)); }
function esc_attr($value) { return htmlspecialchars((string) $value, ENT_QUOTES); }
function esc_url($value) { return (string) $value; }
function get_template_directory_uri() { return 'https://theme.test'; }
function aksrt_get_default_avatar_url() { return 'https://theme.test/default-avatar.png'; }
eval('namespace AKSRT\\\\Settings; class Settings { public static $values = []; public static function get_value($key, $default = null) { return array_key_exists($key, self::$values) ? self::$values[$key] : $default; } }');
\\AKSRT\\Settings\\Settings::$values = json_decode(${JSON.stringify(JSON.stringify(values))}, true);
require ${JSON.stringify(helper)};
${body}
`;
const result = spawnSync('php', ['-r', code], { cwd: root, encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr || result.stdout);
return result.stdout.trim();
}
function sourceFiles(directory) {
const output = [];
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
if (entry.name === '.git' || entry.name === '.claude' || entry.name === 'build' || entry.name === 'vendor' || entry.name === 'node_modules' || entry.name === 'third-party') continue;
if (/\.min\.(?:js|css)$/.test(entry.name)) continue;
const full = path.join(directory, entry.name);
if (entry.isDirectory()) output.push(...sourceFiles(full));
else if (/\.(?:php|js|vue|css)$/.test(entry.name)) output.push(full);
}
return output;
}
test('admin schemas expose the ten Zibll lazy fields and defaults', () => {
const php = read('src/Admin/Schema/GlobalSchema.php');
const vue = read('assets/admin/src/schema/sections/global/common.js');
for (const [key, value] of Object.entries(lazyDefaults)) {
const literal = typeof value === 'string' ? `'${value}'` : String(value);
assert.match(php, new RegExp(`'key'\\s*=>\\s*'${key}'[\\s\\S]*?'default'\\s*=>\\s*${literal}`));
assert.match(vue, new RegExp(`${key}:\\s*${literal}`));
}
assert.match(php, /'key'\s*=>\s*'thumbnail'[\s\S]*?thumbnail\.svg/);
assert.match(vue, /thumbnail:\s*defaultThumbnail/);
});
test('PHP helpers emit placeholder/data-src, direct src, and crawler bypass', () => {
const enabled = phpHarness("echo aksrt_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');");
assert.match(enabled, /class="lazyload fit-cover"/);
assert.match(enabled, /src="https:\/\/theme\.test\/assets\/img\/thumbnail\.svg"/);
assert.match(enabled, /data-src="https:\/\/cdn\.test\/post\.webp"/);
const disabled = phpHarness("echo aksrt_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');", { lazy_posts_thumb: false });
assert.equal(disabled, 'class="fit-cover" src="https://cdn.test/post.webp"');
const crawler = phpHarness("echo aksrt_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');", {}, 'Googlebot/2.1');
assert.equal(crawler, 'class="fit-cover" src="https://cdn.test/post.webp"');
const empty = phpHarness("echo aksrt_get_lazy_attr('lazy_other', '', 'media');");
assert.equal(empty, 'class="media" src=""');
});
test('content, avatar, and responsive image paths follow one contract', () => {
const content = phpHarness(`
$html = '<img class="hero" src="https://cdn.test/photo.jpg" srcset="https://cdn.test/photo-2x.jpg 2x">';
echo aksrt_lazy_content_images($html);
`, { lazy_posts_content: true });
assert.match(content, /thumbnail-lg\.svg/);
assert.match(content, /data-src="https:\/\/cdn\.test\/photo\.jpg"/);
assert.match(content, /data-srcset="https:\/\/cdn\.test\/photo-2x\.jpg 2x"/);
assert.doesNotMatch(content, /\ssrcset=/);
const avatar = phpHarness("echo aksrt_lazy_avatar_html('<img class=\"avatar\" src=\"https://cdn.test/avatar.png\" loading=\"lazy\">');");
assert.match(avatar, /class="lazyload avatar"/);
assert.match(avatar, /src="https:\/\/theme\.test\/default-avatar\.png"/);
assert.match(avatar, /data-src="https:\/\/cdn\.test\/avatar\.png"/);
assert.doesNotMatch(avatar, /loading=/);
});
test('one lazySizes runtime owns dynamic media, backgrounds, videos, and IAS', () => {
const runtime = read('assets/js/lazysizes.js');
const scripts = read('src/Core/ScriptsPart1c.php');
const pagination = read('assets/js/core/pagination.js');
const widgets = read('assets/widgets/frontend.js');
assert.match(scripts, /'aksrt-lazysizes'/);
assert.match(scripts, /'aksrt-rehydrate'[\s\S]*?\['aksrt-lazysizes'\]/);
assert.match(runtime, /lazybeforeunveil/);
assert.match(runtime, /source\[data-src\],source\[data-srcset\]/);
assert.match(runtime, /data-bg/);
assert.match(runtime, /lazyload-action/);
assert.match(runtime, /element\.hasAttribute\('remote-box'\)[\s\S]*?element\.click\(\)/);
assert.doesNotMatch(pagination, /IntersectionObserver|new Image\(/);
assert.doesNotMatch(widgets, /IntersectionObserver/);
});
test('legacy lazy identifiers and theme-native loading attributes are gone', () => {
const files = sourceFiles(root).filter((file) => !file.endsWith('lazyload-contract.test.cjs'));
const source = files.map((file) => fs.readFileSync(file, 'utf8')).join('\n');
const forbidden = [
'data-' + 'lazy-animation',
'lazyload_' + 'enabled',
'get_' + 'lazyload_config',
'aksrt_' + 'lazy_img_attrs',
'aksrt_' + 'lazy_loading',
];
for (const token of forbidden) assert.equal(source.includes(token), false, token);
assert.doesNotMatch(source, /aksrt-lazy(?!sizes)/);
assert.doesNotMatch(source, /aksrt-loaded/);
assert.doesNotMatch(source, /loading=["'](?:lazy|eager)["']/);
assert.doesNotMatch(source, /\.loading\s*=\s*["'](?:lazy|eager)["']/);
assert.doesNotMatch(source, /setAttribute\(\s*["']loading["']/);
});