🖼️ Placeholder Images Creation Guide
Placeholder images покращують perceived performance та user experience під час завантаження контенту. Замість порожнього білого простору або "broken image" icons, користувачі бачать skeleton screens, blur-up placeholders або low-quality image previews. Це створює враження швидшого сайту та зменшує bounce rate. Цей гід охоплює всі типи placeholders та їх імплементацію.
🎯 Types of Placeholders
1. Skeleton Screens
Gray boxes that mimic content structure
Use case: Image galleries, cards, lists
Pros: Modern, progressively loads
2. LQIP (Low Quality Image Placeholder)
Tiny blurred version (5-20KB)
Use case: Hero images, large photos
Pros: Shows actual content, smooth transition
3. Base64 Inline SVG
Embedded SVG placeholder
Use case: Critical above-fold images
Pros: No HTTP request, instant display
4. Color Dominant Placeholder
Solid color або gradient matching image
Use case: Background images
Pros: Minimal размір, smooth transition
💻 Skeleton Screen Implementation
HTML Structure:
<div class="mushroom-card">
<div class="skeleton skeleton-image"></div>
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text"></div>
</div>
CSS Animation:
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
.skeleton-image {
width: 100%;
height: 200px;
margin-bottom: 15px;
}
.skeleton-title {
height: 24px;
width: 70%;
margin-bottom: 10px;
}
.skeleton-text {
height: 16px;
width: 100%;
margin-bottom: 8px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
JavaScript Loading:
// Replace skeleton з real content
function loadMushroomCards() {
fetch('/api/mushrooms')
.then(res => res.json())
.then(data => {
const container = document.querySelector('.gallery');
container.innerHTML = ''; // Remove skeletons
data.forEach(mushroom => {
const card = createMushroomCard(mushroom);
container.appendChild(card);
});
});
}
// Show skeleton while loading
document.addEventListener('DOMContentLoaded', () => {
showSkeletons(6); // Show 6 skeleton cards
loadMushroomCards();
});
🌫️ LQIP (Blur-Up) Implementation
Generating LQIP:
// Sharp (Node.js) - Create tiny blurred version
const sharp = require('sharp');
sharp('mushroom-original.jpg')
.resize(20, 20, { fit: 'inside' }) // Tiny: 20x20px
.blur(5)
.toBuffer()
.then(buffer => {
const base64 = buffer.toString('base64');
console.log(`data:image/jpeg;base64,${base64}`);
});
Progressive Loading HTML:
<div class="progressive-image"
style="background-image:url(data:image/jpeg;base64,/9j/4AAQ...)">
<img
src="mushroom-20x20-blur.jpg"
data-src="mushroom-full.jpg"
class="progressive-image__full"
alt="Psilocybe cubensis"
loading="lazy"
/>
</div>
CSS Blur-Up Effect:
.progressive-image {
position: relative;
overflow: hidden;
background-size: cover;
background-position: center;
filter: blur(10px);
transform: scale(1.05); /* Prevent blur edges */
}
.progressive-image__full {
width: 100%;
opacity: 0;
transition: opacity 0.5s ease;
}
.progressive-image__full.loaded {
opacity: 1;
}
.progressive-image.loaded {
filter: blur(0);
}
JavaScript Blur-Up Logic:
const images = document.querySelectorAll('.progressive-image__full');
images.forEach(img => {
const fullSrc = img.dataset.src;
// Preload full image
const fullImg = new Image();
fullImg.src = fullSrc;
fullImg.onload = () => {
img.src = fullSrc;
img.classList.add('loaded');
img.parentElement.classList.add('loaded');
};
});
🎨 SVG Placeholders
Geometric SVG Placeholder:
<svg viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#8B4513;stop-opacity:1" />
<stop offset="100%" style="stop-color:#D2691E;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="400" height="300" fill="#f5f5f5"/>
<!-- Mushroom silhouette -->
<ellipse cx="200" cy="150" rx="100" ry="40" fill="url(#grad)"/>
<rect x="180" y="150" width="40" height="80" fill="#8B4513"/>
<!-- Loading text -->
<text x="200" y="260" font-family="Arial" font-size="14"
text-anchor="middle" fill="#999">Loading...</text>
</svg>
Minimal Icon Placeholder:
const mushroomPlaceholder = `
<svg width="200" height="200" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="#e0e0e0"/>
<path d="M30,50 Q50,20 70,50" fill="#bdbdbd"/>
<rect x="45" y="50" width="10" height="30" fill="#bdbdbd"/>
</svg>`;
// Inline as base64
const encoded = btoa(mushroomPlaceholder);
imgElement.src = `data:image/svg+xml;base64,${encoded}`;
🌈 Dominant Color Placeholders
Extract Dominant Color (Python):
from PIL import Image
import colorsys
def get_dominant_color(image_path):
img = Image.open(image_path)
img = img.resize((50, 50)) # Downsample
pixels = img.getdata()
# Calculate average RGB
r_avg = sum(p[0] for p in pixels) // len(pixels)
g_avg = sum(p[1] for p in pixels) // len(pixels)
b_avg = sum(p[2] for p in pixels) // len(pixels)
return f"rgb({r_avg}, {g_avg}, {b_avg})"
color = get_dominant_color('mushroom.jpg')
print(color) # "rgb(142, 98, 67)" (earthy brown)
Use Dominant Color:
<div class="image-container"
style="background-color: rgb(142, 98, 67);">
<img src="mushroom.jpg" alt="..." loading="lazy">
</div>
<style>
.image-container {
position: relative;
overflow: hidden;
}
.image-container img {
width: 100%;
display: block;
opacity: 0;
transition: opacity 0.3s;
}
.image-container img.loaded {
opacity: 1;
}
</style>
⚡ Advanced: Blurhash Implementation
Generate Blurhash:
// Install: npm install blurhash
const { encode } = require('blurhash');
const sharp = require('sharp');
async function generateBlurhash(imagePath) {
const imageBuffer = await sharp(imagePath)
.raw()
.ensureAlpha()
.resize(32, 32, { fit: 'inside' })
.toBuffer({ resolveWithObject: true });
const blurhash = encode(
new Uint8ClampedArray(imageBuffer.data),
imageBuffer.info.width,
imageBuffer.info.height,
4,
4
);
return blurhash;
}
// Example output: "LGF5]+Yk^6#M@-5c,1J5@[or[Q6."
Decode та Display:
<!-- Include blurhash library -->
<script src="https://unpkg.com/blurhash/dist/blurhash.min.js"></script>
<canvas id="placeholder" width="400" height="300"></canvas>
<img id="full-image" src="" data-blurhash="LGF5]+Yk^6#M@-5c,1J5@[or[Q6." />
<script>
const canvas = document.getElementById('placeholder');
const img = document.getElementById('full-image');
const blurhash = img.dataset.blurhash;
// Decode blurhash до canvas
const pixels = blurhash.decode(blurhash, 400, 300);
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(400, 300);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
// Load full image
img.onload = () => {
canvas.style.display = 'none';
img.style.display = 'block';
};
img.src = 'mushroom-full.jpg';
</script>
📋 Best Practices
✅ DO
- Match placeholder aspect ratio до real image
- Use subtle animations (не відволікайте)
- Inline critical placeholders (base64)
- Smooth transition (opacity fade)
❌ DON'T
- Large base64 images (> 10KB)
- Jarring layout shifts
- Too aggressive shimmer animation
- Forget width/height attributes
Performance Metrics:
- LQIP size: < 2KB (base64 або ultra-compressed)
- Blurhash size: ~30 bytes (optimal!)
- SVG placeholder: < 1KB
- Skeleton CSS: ~0.5KB
🛠️ Automated Placeholder Generation
Build Script (Node.js):
const sharp = require('sharp');
const { encode } = require('blurhash');
const fs = require('fs');
async function generatePlaceholders(inputDir, outputFile) {
const files = fs.readdirSync(inputDir);
const placeholders = {};
for (const file of files) {
if (!file.match(/\.(jpg|jpeg|png)$/)) continue;
const path = `${inputDir}/${file}`;
// 1. LQIP (20x20 blur)
const lqip = await sharp(path)
.resize(20, 20)
.blur(5)
.toBuffer();
// 2. Dominant color
const { dominant } = await sharp(path).stats();
// 3. Blurhash
const { data, info } = await sharp(path)
.raw()
.ensureAlpha()
.resize(32, 32)
.toBuffer({ resolveWithObject: true });
const blurhash = encode(
new Uint8ClampedArray(data),
info.width,
info.height,
4,
4
);
placeholders[file] = {
lqip: `data:image/jpeg;base64,${lqip.toString('base64')}`,
dominantColor: `rgb(${dominant.r}, ${dominant.g}, ${dominant.b})`,
blurhash: blurhash
};
}
fs.writeFileSync(outputFile, JSON.stringify(placeholders, null, 2));
console.log(`✅ Generated placeholders for ${Object.keys(placeholders).length} images`);
}
generatePlaceholders('./images', './placeholders.json');
Usage:
// Load placeholders data
const placeholders = require('./placeholders.json');
// Render image з placeholder
function renderMushroomImage(filename) {
const placeholder = placeholders[filename];
return `
<div class="progressive-image"
style="background-color: ${placeholder.dominantColor}">
<img
src="${placeholder.lqip}"
data-src="/images/${filename}"
data-blurhash="${placeholder.blurhash}"
alt="Mushroom photo"
loading="lazy"
/>
</div>
`;
}