Imporve image lazy loading

This commit is contained in:
2026-01-28 12:15:08 +01:00
parent 43fc752535
commit 80e3aa95d8
5 changed files with 47 additions and 80 deletions

View File

@@ -10,6 +10,15 @@ if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/../../../');
if (!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT', 1); // we gzip ourself here if (!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT', 1); // we gzip ourself here
require_once(DOKU_INC . 'inc/init.php'); require_once(DOKU_INC . 'inc/init.php');
// Close the session early to prevent blocking concurrent requests.
// PHP sessions are locked by default - if we hold the lock during thumbnail
// generation, all other requests from this user (including page navigation)
// will be blocked until we finish. Since we only need session data for ACL
// checks (which happen before this point via init.php), we can safely close it.
if (function_exists('session_status') && session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
global $INPUT; global $INPUT;
$syntax = plugin_load('syntax', 'luxtools'); $syntax = plugin_load('syntax', 'luxtools');

5
images/placeholder.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@@ -8,46 +8,15 @@
// ============================================================ // ============================================================
// Gallery Thumbnails Module // Gallery Thumbnails Module
// ============================================================ // ============================================================
// Thumbnail loading now relies on native loading="lazy" attribute.
// The browser handles deferred loading, connection limits, and
// automatic cancellation on navigation.
//
// This module is kept as a stub for potential future enhancements.
Luxtools.GalleryThumbnails = (function () { Luxtools.GalleryThumbnails = (function () {
function loadThumb(img) {
var src = img.getAttribute('data-thumb-src') || '';
if (!src) return;
if (img.getAttribute('data-thumb-loading') === '1') return;
img.setAttribute('data-thumb-loading', '1');
var pre = new window.Image();
pre.onload = function () {
img.src = src;
img.removeAttribute('data-thumb-src');
img.removeAttribute('data-thumb-loading');
};
pre.onerror = function () {
img.removeAttribute('data-thumb-loading');
};
pre.src = src;
}
function init() { function init() {
// Handle both gallery and imagebox thumbnails // Native lazy loading handles everything.
var imgs = document.querySelectorAll('div.luxtools-gallery img[data-thumb-src], div.luxtools-imagebox img[data-thumb-src]'); // No JavaScript intervention needed.
if (!imgs || !imgs.length) return;
if ('IntersectionObserver' in window) {
var io = new window.IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
loadThumb(entry.target);
io.unobserve(entry.target);
});
}, { rootMargin: '200px' });
imgs.forEach(function (img) { io.observe(img); });
} else {
// Fallback: load soon after initial render
window.setTimeout(function () {
imgs.forEach(loadThumb);
}, 0);
}
} }
return { return {

View File

@@ -67,17 +67,17 @@ class Output
$genThumbW = (int)max(1, (int)round($thumbW * $thumbScale)); $genThumbW = (int)max(1, (int)round($thumbW * $thumbScale));
$genThumbH = (int)max(1, (int)round($thumbH * $thumbScale)); $genThumbH = (int)max(1, (int)round($thumbH * $thumbScale));
// Build placeholder URL from config (DokuWiki media ID)
$placeholderStyle = '';
$placeholderId = $syntax ? trim((string)$syntax->getConf('thumb_placeholder')) : ''; $placeholderId = $syntax ? trim((string)$syntax->getConf('thumb_placeholder')) : '';
if ($placeholderId !== '' && function_exists('ml')) {
$placeholderUrl = ml($placeholderId, ['w' => $thumbW, 'h' => $thumbH], true, '&');
$placeholderStyle = ' style="--luxtools-placeholder: url(' . hsc($placeholderUrl) . ')"';
}
/** @var \Doku_Renderer_xhtml $renderer */ /** @var \Doku_Renderer_xhtml $renderer */
$renderer = $this->renderer; $renderer = $this->renderer;
$renderer->doc .= '<div class="luxtools-plugin luxtools-gallery" data-luxtools-gallery="1">'; $renderer->doc .= '<div class="luxtools-plugin luxtools-gallery" data-luxtools-gallery="1"' . $placeholderStyle . '>';
global $ID;
$pageId = isset($ID) ? (string)$ID : '';
if (function_exists('cleanID')) {
$pageId = (string)cleanID($pageId);
}
foreach ($this->files as $item) { foreach ($this->files as $item) {
$url = $this->itemWebUrl($item, !empty($params['randlinks'])); $url = $this->itemWebUrl($item, !empty($params['randlinks']));
@@ -88,39 +88,14 @@ class Output
$caption = $label; $caption = $label;
} }
// Use ThumbnailHelper to get thumbnail info // Build thumbnail URL - rely on native loading="lazy" for deferred loading
$imagePath = isset($item['path']) && is_string($item['path']) ? $item['path'] : '';
if ($imagePath !== '' && is_file($imagePath)) {
// Extract root and local from full path for helper
$root = $this->basedir;
$local = '';
if (str_starts_with($imagePath, $root)) {
$local = substr($imagePath, strlen($root));
}
$thumb = ThumbnailHelper::getThumbnail(
$root,
$local,
$pageId,
$genThumbW,
$genThumbH,
$thumbQ,
$placeholderId !== '' ? $placeholderId : null
);
$initialSrc = hsc($thumb['url']);
$dataThumb = $thumb['isFinal'] ? '' : ' data-thumb-src="' . hsc($thumb['thumbUrl']) . '"';
} else {
// Fallback: use URL directly
$thumbUrl = $this->withQueryParams($url, [ $thumbUrl = $this->withQueryParams($url, [
'thumb' => 1, 'thumb' => 1,
'w' => $genThumbW, 'w' => $genThumbW,
'h' => $genThumbH, 'h' => $genThumbH,
'q' => $thumbQ, 'q' => $thumbQ,
]); ]);
$initialSrc = hsc($thumbUrl); $thumbSrc = hsc($thumbUrl);
$dataThumb = '';
}
$renderer->doc .= '<a' $renderer->doc .= '<a'
. ' href="' . $safeUrl . '"' . ' href="' . $safeUrl . '"'
@@ -132,8 +107,7 @@ class Output
. '>'; . '>';
$renderer->doc .= '<img' $renderer->doc .= '<img'
. ' class="luxtools-thumb"' . ' class="luxtools-thumb"'
. ' src="' . $initialSrc . '"' . ' src="' . $thumbSrc . '"'
. $dataThumb
. ' alt=""' . ' alt=""'
. ' width="' . $thumbW . '"' . ' width="' . $thumbW . '"'
. ' height="' . $thumbH . '"' . ' height="' . $thumbH . '"'

View File

@@ -77,6 +77,16 @@ div.luxtools-gallery img.luxtools-thumb {
width: 150px; width: 150px;
height: 150px; height: 150px;
object-fit: cover; object-fit: cover;
/* Placeholder while lazy-loaded image is pending.
* Uses custom property from inline style if thumb_placeholder is configured,
* otherwise falls back to built-in SVG icon. */
background-color: @ini_background;
background-image: var(--luxtools-placeholder, url(images/placeholder.svg));
background-position: center;
background-repeat: no-repeat;
/* contain works well for both: configured placeholder fills the area,
* built-in SVG icon stays small and centered */
background-size: contain;
} }
/* Filename overlay (single line, muted). */ /* Filename overlay (single line, muted). */