Immerse

Immerse

github
email
twitter
youtube
zhihu

画像の遅延読み込み最適化実践ガイド

前言#

起因#

  • 最近、個人ブログを立ち上げましたが、スニペットページには大量の画像が存在し、画像の読み込み体験が非常に悪く、断崖式に感じられます。0 から 1 への過渡期が全くありません(これはページのレイアウトやユーザー体験に大きな影響を与えます。画像の幅と高さが設定されている場合はまだ良いですが、設定されていない場合は画像が高くなる過程があります)。

巧合#

  • この記事を書く準備をしていた日に、フロントエンドの南玖大佬が記事を発表しました。私は「大データはすごい」と叫びました 👍🏻記事:クリックして確認
  • この文章では、他のいくつかのソリューションについて議論します。余談はさておき、本題に入りましょう。
    • 一般的な画像最適化についてはここでは詳しく述べませんが、大まかには以下の通りです:
      • 画像の圧縮、CSS スプライトの使用、遅延読み込み、事前読み込み、CDN キャッシュ、適切な画像フォーマット、七牛 CDN 画像パラメータなど

探索#

  • 以下はこの記事で言及されているいくつかのソリューションです(個人プロジェクトは Next に基づいているため、一部のサンプルコードは React です)。
    • (1)画像の主色調を使用
    • (2)特定の色を使用
    • (3)画像のサムネイルを使用
    • (4)ぼかし + 圧縮画像を使用
    • (5)画像プレースホルダー

方案 1:画像の主色調を使用#

  • 日常の開発では、私たちの画像 src は動的である可能性があり、つまり文字列 string URL です。placeholder="blur" を指定した場合、blurDataURL 属性を追加する必要があります。
import Image from 'next/image';

// Pixel GIF code adapted from https://stackoverflow.com/a/33919020/266535
const keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';

const triplet = (e1: number, e2: number, e3: number) =>
    keyStr.charAt(e1 >> 2) +
    keyStr.charAt(((e1 & 3) << 4) | (e2 >> 4)) +
    keyStr.charAt(((e2 & 15) << 2) | (e3 >> 6)) +
    keyStr.charAt(e3 & 63);

const rgbDataURL = (r: number, g: number, b: number) =>
    `data:image/gif;base64,R0lGODlhAQABAPAA${
        triplet(0, r, g) + triplet(b, 255, 255)
    }/yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==`;

const Color = () => (
    <div>
        <h1>カラー データ URL を使用した画像コンポーネント</h1>
        <Image
            alt="犬"
            src="/dog.jpg"
            placeholder="blur"
            blurDataURL={rgbDataURL(237, 181, 6)}
            width={750}
            height={1000}
            style={{
                maxWidth: '100%',
                height: 'auto'
            }}
        />
        <Image
            alt="猫"
            src="/cat.jpg"
            placeholder="blur"
            blurDataURL={rgbDataURL(2, 129, 210)}
            width={750}
            height={1000}
            style={{
                maxWidth: '100%',
                height: 'auto'
            }}
        />
    </div>
);

export default Color;

方案 2:特定の色を使用#

  • next.config.jsplaceholdercolor に設定し、次に backgroundColor 属性を使用します。
// next.config.js
module.exports = {
    images: {
        placeholder: 'color',
        backgroundColor: '#121212'
    }
};
// 使用
<Image src="/path/to/image.jpg" alt="画像タイトル" width={500} height={500} placeholder="color" />

方案 3: 画像のサムネイルを使用#

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>漸進的な画像読み込み</title>
        <style>
            .placeholder {
                background-color: #f6f6f6;
                background-size: cover;
                background-repeat: no-repeat;
                position: relative;
                overflow: hidden;
            }

            .placeholder img {
                position: absolute;
                opacity: 0;
                top: 0;
                left: 0;
                width: 100%;
                transition: opacity 1s linear;
            }

            .placeholder img.loaded {
                opacity: 1;
            }

            .img-small {
                filter: blur(50px);
                transform: scale(1);
            }
        </style>
    </head>
    <body>
        <div
            class="placeholder"
            data-large="https://qncdn.mopic.mozigu.net/work/143/24/42b204ae3ade4f38/1_sg-uLNm73whmdOgKlrQdZA.jpg"
        >
            <img
                src="https://qncdn.mopic.mozigu.net/work/143/24/5307e9778a944f93/1_sg-uLNm73whmdOgKlrQdZA.jpg"
                class="img-small"
            />
            <div style="padding-bottom: 66.6%"></div>
        </div>
    </body>
</html>
<script>
    window.onload = function () {
        var placeholder = document.querySelector('.placeholder'),
            small = placeholder.querySelector('.img-small');

        // 1. 小さい画像を表示して読み込む
        var img = new Image();
        img.src = small.src;
        img.onload = function () {
            small.classList.add('loaded');
        };

        // 2. 大きい画像を読み込む
        var imgLarge = new Image();
        imgLarge.src = placeholder.dataset.large;
        imgLarge.onload = function () {
            imgLarge.classList.add('loaded');
        };
        placeholder.appendChild(imgLarge);
    };
</script>

方案 4:ぼかし + 圧縮画像を使用#

// progressive-image.tsx
'use client';

import React, { useState, useEffect } from 'react';
import imageCompression from 'browser-image-compression';

interface ProgressiveImageProps {
    src: string;
    alt?: string;
    width?: number;
    height?: number;
    layout?: 'fixed' | 'responsive' | 'fill' | 'intrinsic';
    className?: string;
    style?: React.CSSProperties;
}

export const ProgressiveImage: React.FC<ProgressiveImageProps> = ({
    src,
    alt = '',
    width,
    height,
    layout = 'responsive',
    className = '',
    style = {}
}) => {
    const [currentSrc, setCurrentSrc] = useState<string>(src);
    const [isLoading, setIsLoading] = useState<boolean>(true);
    const [blurLevel, setBlurLevel] = useState<number>(20);

    useEffect(() => {
        let isMounted = true;

        const loadImage = async () => {
            try {
                // 元の画像を読み込み、圧縮する
                const response = await fetch(src);
                const blob = await response.blob();

                // 低品質のプレビュー画像を生成
                const tinyOptions = {
                    maxSizeMB: 0.0002,
                    maxWidthOrHeight: 16,
                    useWebWorker: true,
                    initialQuality: 0.1
                };

                const tinyBlob = await imageCompression(blob, tinyOptions);
                if (isMounted) {
                    const tinyUrl = URL.createObjectURL(tinyBlob);
                    setCurrentSrc(tinyUrl);
                    // ぼかしを徐々に減少させる
                    startSmoothTransition();
                }

                // 元の画像を読み込む
                const highQualityImage = new Image();
                highQualityImage.src = src;
                highQualityImage.onload = () => {
                    if (isMounted) {
                        setCurrentSrc(src);
                        // 高品質の画像が読み込まれたら、スムーズな遷移を続ける
                        setTimeout(() => {
                            setIsLoading(false);
                        }, 100);
                    }
                };
            } catch (error) {
                console.error('画像の読み込みエラー:', error);
                if (isMounted) {
                    setCurrentSrc(src);
                    setIsLoading(false);
                }
            }
        };

        const startSmoothTransition = () => {
            // 20pxのぼかしから10pxに徐々に遷移
            const startBlur = 20;
            const endBlur = 10;
            const duration = 1000; // 1秒
            const steps = 20;
            const stepDuration = duration / steps;
            const blurStep = (startBlur - endBlur) / steps;

            let currentStep = 0;

            const interval = setInterval(() => {
                if (currentStep < steps && isMounted) {
                    setBlurLevel(startBlur - blurStep * currentStep);
                    currentStep++;
                } else {
                    clearInterval(interval);
                }
            }, stepDuration);
        };

        setIsLoading(true);
        setBlurLevel(20);
        loadImage();

        return () => {
            isMounted = false;
            if (currentSrc && currentSrc.startsWith('blob:')) {
                URL.revokeObjectURL(currentSrc);
            }
        };
    }, [src]);

    const getContainerStyle = (): React.CSSProperties => {
        const baseStyle: React.CSSProperties = {
            position: 'relative',
            overflow: 'hidden'
        };

        switch (layout) {
            case 'responsive':
                return {
                    ...baseStyle,
                    maxWidth: width || '100%',
                    width: '100%'
                };
            case 'fixed':
                return {
                    ...baseStyle,
                    width: width,
                    height: height
                };
            case 'fill':
                return {
                    ...baseStyle,
                    width: '100%',
                    height: '100%',
                    position: 'absolute',
                    top: 0,
                    left: 0
                };
            case 'intrinsic':
                return {
                    ...baseStyle,
                    maxWidth: width,
                    width: '100%'
                };
            default:
                return baseStyle;
        }
    };

    const getImageStyle = (): React.CSSProperties => {
        const baseStyle: React.CSSProperties = {
            filter: isLoading ? `blur(${blurLevel}px)` : 'none',
            transition: 'filter 0.8s ease-in-out', // 遷移時間を追加
            transform: 'scale(1.1)', // ぼやけているときにエッジが出ないように少し拡大
            ...style
        };

        switch (layout) {
            case 'responsive':
                return {
                    ...baseStyle,
                    width: '100%',
                    height: 'auto',
                    display: 'block'
                };
            case 'fixed':
                return {
                    ...baseStyle,
                    width: width,
                    height: height
                };
            case 'fill':
                return {
                    ...baseStyle,
                    width: '100%',
                    height: '100%',
                    objectFit: 'cover'
                };
            case 'intrinsic':
                return {
                    ...baseStyle,
                    width: '100%',
                    height: 'auto'
                };
            default:
                return baseStyle;
        }
    };

    return (
        <div className={`${className}`} style={getContainerStyle()}>
            {currentSrc && <img src={currentSrc} alt={alt} style={getImageStyle()} />}
        </div>
    );
};
// 使用
<ProgressiveImage
    src={photo}
    alt={short.title}
    width={300}
    height={250}
    layout="responsive"
    className="h-full min-h-[150px]"
/>

方案 5:画像プレースホルダー#

  • Next.js の next/image コンポーネントの placeholder 属性は、blur というオプションを提供します。デフォルトは empty です。
    • blur はぼかしのプレビュー画像を生成します(ただし、このオプションはぼかし画像を生成するのに時間がかかるため、初期読み込みの実践が増加します)。
    • 注意:placeholder="blur" の場合、画像を静的にインポートする方法を使用する必要があります。そうしないと、Next.js は画像の漸進的な読み込みの前処理を行いません。
import Image from 'next/image';
import mountains from '/public/mountains.jpg';

const PlaceholderBlur = () => (
    <div>
        <h1>プレースホルダーぼかしを使用した画像コンポーネント</h1>
        <Image
            alt="山々"
            src={mountains}
            placeholder="blur"
            width={700}
            height={475}
            style={{
                maxWidth: '100%',
                height: 'auto'
            }}
        />
    </div>
);

export default PlaceholderBlur;

総括#

  • 製品の第一印象は非常に重要で、良好なユーザー体験は製品にとって必要不可欠です。
  • 読んでいただきありがとうございます。また次回お会いしましょう!
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。