pin_drop当前位置:知识文库 ❯ 图文

JS集成:HTML onload事件:w - 详细教程与实战指南

教程简介

页面加载是Web应用生命周期的起点,理解各种加载事件的触发时机和区别,对于正确初始化应用、优化加载性能至关重要。onload事件是最常用的页面加载事件,但它只是众多加载事件之一。DOMContentLoaded在DOM树构建完成时触发,远早于load事件;readystatechange提供了更细粒度的加载状态追踪;图片和子资源的加载也有独立的事件机制。本教程将全面讲解HTML onload及相关加载事件,帮助开发者掌握页面加载的完整生命周期,实现高效的资源加载策略。


核心概念

页面加载的完整生命周期

浏览器加载一个HTML页面的过程如下:

代码示例

1. 开始解析HTML
   |
2. 遇到<link>、<script>等资源,开始下载
   |
3. HTML解析完成,DOM树构建完毕
   |
4. DOMContentLoaded 事件触发(此时CSS和图片可能还未加载完成)
   |
5. CSS加载完成,样式应用
   |
6. 图片等子资源加载完成
   |
7. load 事件触发(页面及所有资源全部加载完成)
   |
8. 用户交互开始

三种核心加载事件对比

事件 触发时机 等待资源 常见用途
DOMContentLoaded DOM树构建完成 不等待CSS、图片、iframe 初始化DOM操作、绑定事件
load 页面及所有资源加载完成 等待CSS、图片、iframe等 依赖资源尺寸的操作
readystatechange document.readyState变化时 取决于readyState值 细粒度加载状态追踪

document.readyState的值

值 说明 对应阶段
loading 文档正在加载中 HTML解析中
interactive 文档解析完成,但子资源可能仍在加载 DOM构建完毕(与DOMContentLoaded同时)
complete 文档和所有子资源加载完成 全部加载完毕(与load同时)

语法与用法

window.onload

代码示例

// 方式1:属性赋值(后者覆盖前者)
window.onload = function() {
    console.log('页面加载完成');
};

// 方式2:addEventListener(推荐,可添加多个)
window.addEventListener('load', function() {
    console.log('页面加载完成');
});

DOMContentLoaded

代码示例

document.addEventListener('DOMContentLoaded', function() {
    console.log('DOM构建完成,可以操作DOM了');
});

readystatechange

代码示例

document.addEventListener('readystatechange', function() {
    console.log('readyState:', document.readyState);
    if (document.readyState === 'interactive') {
        console.log('文档解析完成');
    }
    if (document.readyState === 'complete') {
        console.log('全部加载完成');
    }
});

图片和资源加载事件

代码示例

// 图片加载
const img = new Image();
img.onload = function() { console.log('图片加载完成', img.naturalWidth, img.naturalHeight); };
img.onerror = function() { console.log('图片加载失败'); };
img.src = 'image.jpg';

// script加载
const script = document.createElement('script');
script.onload = function() { console.log('脚本加载完成'); };
script.src = 'app.js';

// link样式加载
const link = document.createElement('link');
link.rel = 'stylesheet';
link.onload = function() { console.log('样式加载完成'); };
link.href = 'style.css';

代码示例

示例1:DOMContentLoaded与load事件对比

通过可视化时间线,直观观察各加载事件的实际触发顺序和时间差:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DOMContentLoaded与load事件对比</title>
    <style>
        body {
            font-family: 'Microsoft YaHei', sans-serif;
            max-width: 800px;
            margin: 40px auto;
            padding: 20px;
        }
        .timeline {
            position: relative;
            padding: 20px 0;
            margin: 20px 0;
        }
        .timeline::before {
            content: '';
            position: absolute;
            left: 30px;
            top: 0;
            bottom: 0;
            width: 3px;
            background: #e0e0e0;
        }
        .timeline-item {
            position: relative;
            padding: 12px 20px 12px 70px;
            margin: 8px 0;
            border-radius: 8px;
            transition: all 0.3s;
        }
        .timeline-item::before {
            content: '';
            position: absolute;
            left: 22px;
            top: 16px;
            width: 18px;
            height: 18px;
            border-radius: 50%;
            background: #e0e0e0;
            border: 3px solid white;
            box-shadow: 0 0 0 2px #e0e0e0;
            transition: all 0.3s;
        }
        .timeline-item.fired {
            background: #f0f7ff;
        }
        .timeline-item.fired::before {
            background: #2196F3;
            box-shadow: 0 0 0 2px #2196F3;
        }
        .timeline-item .event-name {
            font-weight: bold;
            color: #333;
        }
        .timeline-item .event-time {
            font-size: 13px;
            color: #666;
            font-family: 'Consolas', monospace;
        }
        .timeline-item .event-desc {
            font-size: 13px;
            color: #888;
            margin-top: 4px;
        }
        .info-box {
            background: white;
            border-radius: 10px;
            padding: 20px;
            margin: 20px 0;
            box-shadow: 0 2px 10px rgba(0,0,0,0.08);
        }
        .compare-table {
            width: 100%;
            border-collapse: collapse;
            margin: 12px 0;
        }
        .compare-table th, .compare-table td {
            padding: 10px 14px;
            text-align: left;
            border-bottom: 1px solid #eee;
        }
        .compare-table th {
            background: #f5f5f5;
            font-weight: bold;
        }
        .tag-yes { color: #4CAF50; font-weight: bold; }
        .tag-no { color: #f44336; }
    </style>
</head>
<body>
    <h1>DOMContentLoaded 与 load 事件对比</h1>

    <div class="info-box">
        <h3>事件对比表</h3>
        <table class="compare-table">
            <tr>
                <th>特性</th>
                <th>DOMContentLoaded</th>
                <th>load</th>
            </tr>
            <tr>
                <td>触发时机</td>
                <td>DOM树构建完成</td>
                <td>所有资源加载完成</td>
            </tr>
            <tr>
                <td>等待CSS</td>
                <td class="tag-no">不等待</td>
                <td class="tag-yes">等待</td>
            </tr>
            <tr>
                <td>等待图片</td>
                <td class="tag-no">不等待</td>
                <td class="tag-yes">等待</td>
            </tr>
            <tr>
                <td>等待iframe</td>
                <td class="tag-no">不等待</td>
                <td class="tag-yes">等待</td>
            </tr>
            <tr>
                <td>等待async脚本</td>
                <td class="tag-no">不等待</td>
                <td class="tag-yes">等待</td>
            </tr>
            <tr>
                <td>绑定对象</td>
                <td>document</td>
                <td>window</td>
            </tr>
            <tr>
                <td>推荐用途</td>
                <td>DOM操作初始化</td>
                <td>依赖资源尺寸的操作</td>
            </tr>
        </table>
    </div>

    <h2>事件触发时间线</h2>
    <p>观察以下事件的实际触发顺序和时间差</p>

    <div class="timeline" id="timeline">
        <div class="timeline-item" id="item-start">
            <div class="event-name">页面开始加载</div>
            <div class="event-time" id="time-start">-</div>
            <div class="event-desc">浏览器开始解析HTML</div>
        </div>
        <div class="timeline-item" id="item-dom">
            <div class="event-name">DOMContentLoaded</div>
            <div class="event-time" id="time-dom">-</div>
            <div class="event-desc">DOM树构建完成,可以安全操作DOM</div>
        </div>
        <div class="timeline-item" id="item-interactive">
            <div class="event-name">readystatechange (interactive)</div>
            <div class="event-time" id="time-interactive">-</div>
            <div class="event-desc">document.readyState变为interactive</div>
        </div>
        <div class="timeline-item" id="item-load">
            <div class="event-name">load</div>
            <div class="event-time" id="time-load">-</div>
            <div class="event-desc">页面及所有子资源加载完成</div>
        </div>
        <div class="timeline-item" id="item-complete">
            <div class="event-name">readystatechange (complete)</div>
            <div class="event-time" id="time-complete">-</div>
            <div class="event-desc">document.readyState变为complete</div>
        </div>
    </div>

    <!-- 使用一张较大的图片来制造加载延迟 -->
    <img src="https://picsum.photos/2000/1500?random=1" style="display:none;" id="testImage">

    <script>
        const pageStart = performance.timeOrigin || Date.now();
        document.getElementById('time-start').textContent = '0ms (页面开始)';
        document.getElementById('item-start').classList.add('fired');

        // readystatechange - interactive
        document.addEventListener('readystatechange', function() {
            if (document.readyState === 'interactive') {
                const elapsed = Math.round(performance.now());
                document.getElementById('time-interactive').textContent = elapsed + 'ms';
                document.getElementById('item-interactive').classList.add('fired');
            }
        });

        // DOMContentLoaded
        document.addEventListener('DOMContentLoaded', function() {
            const elapsed = Math.round(performance.now());
            document.getElementById('time-dom').textContent = elapsed + 'ms';
            document.getElementById('item-dom').classList.add('fired');
        });

        // readystatechange - complete
        document.addEventListener('readystatechange', function() {
            if (document.readyState === 'complete') {
                const elapsed = Math.round(performance.now());
                document.getElementById('time-complete').textContent = elapsed + 'ms';
                document.getElementById('item-complete').classList.add('fired');
            }
        });

        // load
        window.addEventListener('load', function() {
            const elapsed = Math.round(performance.now());
            document.getElementById('time-load').textContent = elapsed + 'ms';
            document.getElementById('item-load').classList.add('fired');
        });
    </script>
</body>
</html>

示例2:图片和资源加载事件

追踪图片加载状态,并演示动态加载CSS、JS和图片资源的事件处理:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片和资源加载事件</title>
    <style>
        body {
            font-family: 'Microsoft YaHei', sans-serif;
            max-width: 800px;
            margin: 40px auto;
            padding: 20px;
        }
        .image-grid {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 16px;
            margin: 20px 0;
        }
        .image-card {
            background: white;
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 2px 10px rgba(0,0,0,0.08);
        }
        .image-card .img-wrapper {
            height: 160px;
            background: #f0f0f0;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
            overflow: hidden;
        }
        .image-card img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            opacity: 0;
            transition: opacity 0.3s;
        }
        .image-card img.loaded { opacity: 1; }
        .image-card .loading-indicator {
            position: absolute;
            color: #999;
            font-size: 14px;
        }
        .image-card .status-bar {
            padding: 10px 14px;
            font-size: 13px;
        }
        .status-loading { color: #FF9800; }
        .status-loaded { color: #4CAF50; }
        .status-error { color: #f44336; }
        .progress-bar {
            height: 4px;
            background: #e0e0e0;
            border-radius: 2px;
            overflow: hidden;
            margin-top: 6px;
        }
        .progress-bar .fill {
            height: 100%;
            background: #4CAF50;
            width: 0;
            transition: width 0.3s;
        }
        .resource-panel {
            background: white;
            border-radius: 10px;
            padding: 20px;
            margin: 20px 0;
            box-shadow: 0 2px 10px rgba(0,0,0,0.08);
        }
        .resource-item {
            display: flex;
            align-items: center;
            gap: 12px;
            padding: 10px 0;
            border-bottom: 1px solid #f0f0f0;
        }
        .resource-icon {
            width: 36px;
            height: 36px;
            border-radius: 6px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 18px;
        }
        .icon-css { background: #E3F2FD; }
        .icon-js { background: #FFF3E0; }
        .icon-img { background: #E8F5E9; }
        .resource-info { flex: 1; }
        .resource-name { font-size: 14px; font-weight: bold; }
        .resource-status { font-size: 12px; color: #999; }
    </style>
</head>
<body>
    <h1>图片和资源加载事件</h1>

    <h2>图片加载状态追踪</h2>
    <div class="image-grid" id="imageGrid">
        <!-- 动态生成 -->
    </div>

    <h2>动态资源加载</h2>
    <div class="resource-panel">
        <button onclick="loadCSS()" style="padding:8px 16px;border:none;border-radius:6px;cursor:pointer;background:#2196F3;color:white;margin:4px;">加载CSS</button>
        <button onclick="loadJS()" style="padding:8px 16px;border:none;border-radius:6px;cursor:pointer;background:#FF9800;color:white;margin:4px;">加载JS</button>
        <button onclick="loadImage()" style="padding:8px 16px;border:none;border-radius:6px;cursor:pointer;background:#4CAF50;color:white;margin:4px;">加载图片</button>
        <div id="resourceList" style="margin-top:16px;"></div>
    </div>

    <script>
        // ========== 图片加载追踪 ==========
        const imageGrid = document.getElementById('imageGrid');
        const images = [
            { url: 'https://picsum.photos/400/300?random=10', name: '风景图' },
            { url: 'https://picsum.photos/400/300?random=20', name: '建筑图' },
            { url: 'https://picsum.photos/400/300?random=30', name: '自然图' },
            { url: 'https://httpbin.org/image/notfound', name: '错误图(404)' },
            { url: 'https://picsum.photos/400/300?random=50', name: '城市图' },
            { url: 'https://picsum.photos/400/300?random=60', name: '动物图' }
        ];

        images.forEach(function(imgData, index) {
            const card = document.createElement('div');
            card.className = 'image-card';
            card.innerHTML = '<div class="img-wrapper">' +
                '<span class="loading-indicator" id="indicator-' + index + '">加载中...</span>' +
                '<img id="img-' + index + '" alt="' + imgData.name + '">' +
                '</div>' +
                '<div class="status-bar">' +
                '<div id="status-' + index + '" class="status-loading">' + imgData.name + ' - 等待加载</div>' +
                '<div class="progress-bar"><div class="fill" id="progress-' + index + '"></div></div>' +
                '</div>';
            imageGrid.appendChild(card);

            const img = card.querySelector('#img-' + index);
            const startTime = performance.now();

            // 模拟进度
            let progressInterval = setInterval(function() {
                const elapsed = performance.now() - startTime;
                const fakeProgress = Math.min(90, elapsed / 20);
                document.getElementById('progress-' + index).style.width = fakeProgress + '%';
            }, 100);

            img.onload = function() {
                clearInterval(progressInterval);
                const loadTime = Math.round(performance.now() - startTime);
                img.classList.add('loaded');
                document.getElementById('indicator-' + index).style.display = 'none';
                document.getElementById('status-' + index).className = 'status-loaded';
                document.getElementById('status-' + index).textContent = imgData.name + ' - 加载完成 (' + loadTime + 'ms, ' + img.naturalWidth + 'x' + img.naturalHeight + ')';
                document.getElementById('progress-' + index).style.width = '100%';
            };

            img.onerror = function() {
                clearInterval(progressInterval);
                document.getElementById('indicator-' + index).textContent = '加载失败';
                document.getElementById('indicator-' + index).style.color = '#f44336';
                document.getElementById('status-' + index).className = 'status-error';
                document.getElementById('status-' + index).textContent = imgData.name + ' - 加载失败';
                document.getElementById('progress-' + index).style.width = '100%';
                document.getElementById('progress-' + index).style.background = '#f44336';
            };

            // 延迟加载,便于观察
            setTimeout(function() {
                img.src = imgData.url;
                document.getElementById('status-' + index).textContent = imgData.name + ' - 加载中...';
            }, index * 300);
        });

        // ========== 动态资源加载 ==========
        const resourceList = document.getElementById('resourceList');

        function addResourceEntry(type, name, status) {
            const icons = { css: '[CSS]', js: '[JS]', img: '[IMG]' };
            const entry = document.createElement('div');
            entry.className = 'resource-item';
            entry.id = 'res-' + name.replace(/[^a-z0-9]/gi, '');
            entry.innerHTML = '<div class="resource-icon icon-' + type + '">' + icons[type] + '</div>' +
                '<div class="resource-info">' +
                '<div class="resource-name">' + name + '</div>' +
                '<div class="resource-status">' + status + '</div>' +
                '</div>';
            resourceList.appendChild(entry);
            return entry;
        }

        function loadCSS() {
            const entry = addResourceEntry('css', '动态CSS', '加载中...');
            const link = document.createElement('link');
            link.rel = 'stylesheet';
            link.href = 'https://cdn.jsdelivr.net/npm/water.css@2/out/light.min.css';
            link.onload = function() {
                entry.querySelector('.resource-status').textContent = '加载完成';
                entry.querySelector('.resource-status').style.color = '#4CAF50';
            };
            link.onerror = function() {
                entry.querySelector('.resource-status').textContent = '加载失败';
                entry.querySelector('.resource-status').style.color = '#f44336';
            };
            document.head.appendChild(link);
        }

        function loadJS() {
            const entry = addResourceEntry('js', '动态JS', '加载中...');
            const script = document.createElement('script');
            script.src = 'https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js';
            script.onload = function() {
                entry.querySelector('.resource-status').textContent = '加载完成 (lodash ' + _.VERSION + ')';
                entry.querySelector('.resource-status').style.color = '#4CAF50';
            };
            script.onerror = function() {
                entry.querySelector('.resource-status').textContent = '加载失败';
                entry.querySelector('.resource-status').style.color = '#f44336';
            };
            document.head.appendChild(script);
        }

        function loadImage() {
            const entry = addResourceEntry('img', '动态图片', '加载中...');
            const img = new Image();
            img.src = 'https://picsum.photos/800/600?random=' + Date.now();
            img.onload = function() {
                entry.querySelector('.resource-status').textContent = '加载完成 (' + img.naturalWidth + 'x' + img.naturalHeight + ')';
                entry.querySelector('.resource-status').style.color = '#4CAF50';
            };
            img.onerror = function() {
                entry.querySelector('.resource-status').textContent = '加载失败';
                entry.querySelector('.resource-status').style.color = '#f44336';
            };
        }
    </script>
</body>
</html>

示例3:加载顺序与性能优化

使用Performance API获取页面加载性能指标,并展示script标签的不同加载策略:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>加载顺序与性能优化</title>
    <style>
        body {
            font-family: 'Microsoft YaHei', sans-serif;
            max-width: 800px;
            margin: 40px auto;
            padding: 20px;
        }
        .perf-card {
            background: white;
            border-radius: 10px;
            padding: 20px;
            margin: 16px 0;
            box-shadow: 0 2px 10px rgba(0,0,0,0.08);
        }
        .metric-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
            gap: 12px;
            margin: 16px 0;
        }
        .metric-item {
            background: #f8f9fa;
            padding: 14px;
            border-radius: 8px;
            text-align: center;
        }
        .metric-item .value {
            font-size: 24px;
            font-weight: bold;
            color: #1565C0;
            font-family: 'Consolas', monospace;
        }
        .metric-item .label {
            font-size: 12px;
            color: #666;
            margin-top: 4px;
        }
        .tip-box {
            background: #E3F2FD;
            border-left: 4px solid #2196F3;
            padding: 14px 18px;
            border-radius: 0 8px 8px 0;
            margin: 12px 0;
            font-size: 14px;
        }
        .tip-box code {
            background: rgba(0,0,0,0.06);
            padding: 2px 6px;
            border-radius: 3px;
            font-size: 13px;
        }
        .code-block {
            background: #263238;
            color: #ECEFF1;
            padding: 16px;
            border-radius: 8px;
            font-family: 'Consolas', monospace;
            font-size: 13px;
            overflow-x: auto;
            margin: 12px 0;
            line-height: 1.6;
        }
        .code-block .comment { color: #6A9955; }
        .code-block .keyword { color: #569CD6; }
        .code-block .string { color: #CE9178; }
    </style>
</head>
<body>
    <h1>加载顺序与性能优化</h1>

    <div class="perf-card">
        <h2>当前页面性能指标</h2>
        <div class="metric-grid" id="metrics">
            <div class="metric-item">
                <div class="value" id="metricDNS">-</div>
                <div class="label">DNS查询 (ms)</div>
            </div>
            <div class="metric-item">
                <div class="value" id="metricTCP">-</div>
                <div class="label">TCP连接 (ms)</div>
            </div>
            <div class="metric-item">
                <div class="value" id="metricTTFB">-</div>
                <div class="label">首字节时间 (ms)</div>
            </div>
            <div class="metric-item">
                <div class="value" id="metricDOM">-</div>
                <div class="label">DOM解析 (ms)</div>
            </div>
            <div class="metric-item">
                <div class="value" id="metricLoad">-</div>
                <div class="label">页面加载 (ms)</div>
            </div>
            <div class="metric-item">
                <div class="value" id="metricDOMReady">-</div>
                <div class="label">DOM Ready (ms)</div>
            </div>
        </div>
    </div>

    <div class="perf-card">
        <h2>script标签加载策略</h2>

        <div class="tip-box">
            <strong>默认行为:</strong> &lt;script&gt; 标签会阻塞HTML解析,直到脚本下载并执行完毕
        </div>

        <div class="code-block">
<span class="comment">// 方式1:默认 - 阻塞解析</span>
&lt;<span class="keyword">script</span> src=<span class="string">"app.js"</span>&gt;&lt;/<span class="keyword">script</span>&gt;

<span class="comment">// 方式2:defer - 不阻塞解析,DOM构建完成后按顺序执行</span>
&lt;<span class="keyword">script</span> <span class="keyword">defer</span> src=<span class="string">"app.js"</span>&gt;&lt;/<span class="keyword">script</span>&gt;

<span class="comment">// 方式3:async - 不阻塞解析,下载完立即执行(无序)</span>
&lt;<span class="keyword">script</span> <span class="keyword">async</span> src=<span class="string">"analytics.js"</span>&gt;&lt;/<span class="keyword">script</span>&gt;

<span class="comment">// 方式4:动态创建 - 类似async,可控制执行时机</span>
<span class="keyword">const</span> script = document.createElement(<span class="string">'script'</span>);
script.src = <span class="string">'app.js'</span>;
document.head.appendChild(script);
        </div>

        <table style="width:100%;border-collapse:collapse;margin:12px 0;">
            <tr style="background:#f5f5f5;">
                <th style="padding:10px;text-align:left;">属性</th>
                <th style="padding:10px;text-align:left;">阻塞解析</th>
                <th style="padding:10px;text-align:left;">执行顺序</th>
                <th style="padding:10px;text-align:left;">触发DOMContentLoaded</th>
            </tr>
            <tr>
                <td style="padding:10px;">默认</td>
                <td style="padding:10px;color:#f44336;">阻塞</td>
                <td style="padding:10px;">按文档顺序</td>
                <td style="padding:10px;">等待执行完</td>
            </tr>
            <tr style="background:#fafafa;">
                <td style="padding:10px;">defer</td>
                <td style="padding:10px;color:#4CAF50;">不阻塞</td>
                <td style="padding:10px;">按文档顺序</td>
                <td style="padding:10px;">等待执行完</td>
            </tr>
            <tr>
                <td style="padding:10px;">async</td>
                <td style="padding:10px;color:#4CAF50;">不阻塞</td>
                <td style="padding:10px;">下载完即执行(无序)</td>
                <td style="padding:10px;">不等待</td>
            </tr>
        </table>
    </div>

    <div class="perf-card">
        <h2>优化建议</h2>
        <div class="tip-box">
            <strong>1. CSS放在head中:</strong> CSS会阻塞渲染,尽早加载可避免闪烁(FOUC)
        </div>
        <div class="tip-box">
            <strong>2. JS放在body底部或使用defer:</strong> 避免阻塞DOM解析
        </div>
        <div class="tip-box">
            <strong>3. 使用preload预加载关键资源:</strong><br>
            <code>&lt;link rel="preload" href="critical.js" as="script"&gt;</code>
        </div>
        <div class="tip-box">
            <strong>4. 使用prefetch预获取下一页资源:</strong><br>
            <code>&lt;link rel="prefetch" href="next-page.js"&gt;</code>
        </div>
        <div class="tip-box">
            <strong>5. 图片使用loading="lazy"懒加载:</strong><br>
            <code>&lt;img src="image.jpg" loading="lazy"&gt;</code>
        </div>
    </div>

    <script>
        // 使用Performance API获取加载指标
        window.addEventListener('load', function() {
            setTimeout(function() {
                const timing = performance.timing;
                if (timing) {
                    const dns = timing.domainLookupEnd - timing.domainLookupStart;
                    const tcp = timing.connectEnd - timing.connectStart;
                    const ttfb = timing.responseStart - timing.requestStart;
                    const domParsing = timing.domInteractive - timing.domLoading;
                    const pageLoad = timing.loadEventEnd - timing.navigationStart;
                    const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;

                    document.getElementById('metricDNS').textContent = dns;
                    document.getElementById('metricTCP').textContent = tcp;
                    document.getElementById('metricTTFB').textContent = ttfb;
                    document.getElementById('metricDOM').textContent = domParsing;
                    document.getElementById('metricLoad').textContent = pageLoad;
                    document.getElementById('metricDOMReady').textContent = domReady;
                }
            }, 100);
        });
    </script>
</body>
</html>

示例4:图片懒加载实现

展示三种不同的图片懒加载实现方式:原生loading="lazy"、IntersectionObserver和scroll事件:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片懒加载实现</title>
    <style>
        body {
            font-family: 'Microsoft YaHei', sans-serif;
            max-width: 800px;
            margin: 40px auto;
            padding: 20px;
        }
        .lazy-image {
            background: #f0f0f0;
            border-radius: 10px;
            margin: 20px 0;
            overflow: hidden;
            position: relative;
        }
        .lazy-image img {
            width: 100%;
            height: 300px;
            object-fit: cover;
            display: block;
            opacity: 0;
            transition: opacity 0.5s ease;
        }
        .lazy-image img.loaded { opacity: 1; }
        .lazy-image .placeholder {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: #999;
            font-size: 14px;
        }
        .lazy-image .load-info {
            position: absolute;
            bottom: 10px;
            right: 10px;
            background: rgba(0,0,0,0.6);
            color: white;
            padding: 4px 10px;
            border-radius: 4px;
            font-size: 12px;
            font-family: 'Consolas', monospace;
        }
        .method-section {
            background: white;
            border-radius: 10px;
            padding: 20px;
            margin: 20px 0;
            box-shadow: 0 2px 10px rgba(0,0,0,0.08);
        }
        .method-badge {
            display: inline-block;
            padding: 4px 12px;
            border-radius: 4px;
            font-size: 12px;
            font-weight: bold;
        }
        .badge-native { background: #E8F5E9; color: #2E7D32; }
        .badge-io { background: #E3F2FD; color: #1565C0; }
        .badge-scroll { background: #FFF3E0; color: #E65100; }
    </style>
</head>
<body>
    <h1>图片懒加载实现</h1>

    <!-- 方法1:原生 loading="lazy" -->
    <div class="method-section">
        <h2>方法1:原生 loading="lazy" <span class="method-badge badge-native">推荐</span></h2>
        <p>最简单的懒加载方式,浏览器原生支持</p>
    </div>

    <div class="lazy-image">
        <img src="https://picsum.photos/800/300?random=101"
             loading="lazy"
             alt="懒加载图片1"
             onload="this.classList.add('loaded'); this.parentElement.querySelector('.load-info').textContent='原生lazy加载完成'">
        <span class="placeholder">等待滚动到可视区域...</span>
        <span class="load-info">未加载</span>
    </div>
    <div class="lazy-image">
        <img src="https://picsum.photos/800/300?random=102"
             loading="lazy"
             alt="懒加载图片2"
             onload="this.classList.add('loaded'); this.parentElement.querySelector('.load-info').textContent='原生lazy加载完成'">
        <span class="placeholder">等待滚动到可视区域...</span>
        <span class="load-info">未加载</span>
    </div>
    <div class="lazy-image">
        <img src="https://picsum.photos/800/300?random=103"
             loading="lazy"
             alt="懒加载图片3"
             onload="this.classList.add('loaded'); this.parentElement.querySelector('.load-info').textContent='原生lazy加载完成'">
        <span class="placeholder">等待滚动到可视区域...</span>
        <span class="load-info">未加载</span>
    </div>

    <!-- 方法2:IntersectionObserver -->
    <div class="method-section">
        <h2>方法2:IntersectionObserver <span class="method-badge badge-io">灵活</span></h2>
        <p>更灵活的控制,可自定义触发阈值和根边距</p>
    </div>

    <div class="lazy-image" data-lazy="io">
        <img data-src="https://picsum.photos/800/300?random=201" alt="IO懒加载1">
        <span class="placeholder">IntersectionObserver 等待中...</span>
        <span class="load-info">未加载</span>
    </div>
    <div class="lazy-image" data-lazy="io">
        <img data-src="https://picsum.photos/800/300?random=202" alt="IO懒加载2">
        <span class="placeholder">IntersectionObserver 等待中...</span>
        <span class="load-info">未加载</span>
    </div>
    <div class="lazy-image" data-lazy="io">
        <img data-src="https://picsum.photos/800/300?random=203" alt="IO懒加载3">
        <span class="placeholder">IntersectionObserver 等待中...</span>
        <span class="load-info">未加载</span>
    </div>

    <!-- 方法3:scroll事件 -->
    <div class="method-section">
        <h2>方法3:scroll事件 + getBoundingClientRect <span class="method-badge badge-scroll">兼容</span></h2>
        <p>传统方式,兼容性最好但性能较差</p>
    </div>

    <div class="lazy-image" data-lazy="scroll">
        <img data-src="https://picsum.photos/800/300?random=301" alt="Scroll懒加载1">
        <span class="placeholder">Scroll事件 等待中...</span>
        <span class="load-info">未加载</span>
    </div>
    <div class="lazy-image" data-lazy="scroll">
        <img data-src="https://picsum.photos/800/300?random=302" alt="Scroll懒加载2">
        <span class="placeholder">Scroll事件 等待中...</span>
        <span class="load-info">未加载</span>
    </div>
    <div class="lazy-image" data-lazy="scroll">
        <img data-src="https://picsum.photos/800/300?random=303" alt="Scroll懒加载3">
        <span class="placeholder">Scroll事件 等待中...</span>
        <span class="load-info">未加载</span>
    </div>

    <script>
        // ========== 方法2:IntersectionObserver ==========
        const ioObserver = new IntersectionObserver(function(entries) {
            entries.forEach(function(entry) {
                if (entry.isIntersecting) {
                    const container = entry.target;
                    const img = container.querySelector('img');
                    const src = img.dataset.src;
                    if (src) {
                        img.src = src;
                        img.onload = function() {
                            img.classList.add('loaded');
                            container.querySelector('.placeholder').style.display = 'none';
                            container.querySelector('.load-info').textContent = 'IO加载完成';
                        };
                        img.removeAttribute('data-src');
                    }
                    ioObserver.unobserve(container);
                }
            });
        }, {
            rootMargin: '100px', // 提前100px开始加载
            threshold: 0.1       // 10%可见时触发
        });

        document.querySelectorAll('[data-lazy="io"]').forEach(function(el) {
            ioObserver.observe(el);
        });

        // ========== 方法3:scroll事件 ==========
        function checkScrollLazyImages() {
            const images = document.querySelectorAll('[data-lazy="scroll"]');
            images.forEach(function(container) {
                const img = container.querySelector('img');
                if (img.dataset.src) {
                    const rect = container.getBoundingClientRect();
                    const isVisible = rect.top < window.innerHeight + 100 && rect.bottom > -100;
                    if (isVisible) {
                        img.src = img.dataset.src;
                        img.onload = function() {
                            img.classList.add('loaded');
                            container.querySelector('.placeholder').style.display = 'none';
                            container.querySelector('.load-info').textContent = 'Scroll加载完成';
                        };
                        img.removeAttribute('data-src');
                    }
                }
            });
        }

        // 节流处理
        let scrollTimer = null;
        window.addEventListener('scroll', function() {
            if (scrollTimer) return;
            scrollTimer = setTimeout(function() {
                checkScrollLazyImages();
                scrollTimer = null;
            }, 200);
        }, { passive: true });

        // 初始检查
        checkScrollLazyImages();
    </script>
</body>
</html>

示例5:readystatechange与加载状态追踪

通过可视化仪表板实时追踪document.readyState的状态变化:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>readystatechange与加载状态追踪</title>
    <style>
        body {
            font-family: 'Microsoft YaHei', sans-serif;
            max-width: 800px;
            margin: 40px auto;
            padding: 20px;
        }
        .state-dashboard {
            background: white;
            border-radius: 12px;
            padding: 24px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
            margin: 20px 0;
        }
        .state-indicator {
            display: flex;
            align-items: center;
            gap: 16px;
            padding: 16px;
            margin: 8px 0;
            border-radius: 8px;
            background: #f8f9fa;
            transition: all 0.3s;
        }
        .state-indicator.active {
            background: #E8F5E9;
            border-left: 4px solid #4CAF50;
        }
        .state-indicator.current {
            background: #E3F2FD;
            border-left: 4px solid #2196F3;
            box-shadow: 0 2px 8px rgba(33,150,243,0.2);
        }
        .state-dot {
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background: #e0e0e0;
            transition: background 0.3s;
        }
        .state-indicator.active .state-dot { background: #4CAF50; }
        .state-indicator.current .state-dot {
            background: #2196F3;
            animation: pulse 1.5s infinite;
        }
        @keyframes pulse {
            0%, 100% { box-shadow: 0 0 0 0 rgba(33,150,243,0.4); }
            50% { box-shadow: 0 0 0 8px rgba(33,150,243,0); }
        }
        .state-name { font-weight: bold; min-width: 120px; }
        .state-desc { color: #666; font-size: 14px; }
        .state-time { margin-left: auto; font-family: 'Consolas', monospace; color: #1565C0; }
        .event-log {
            background: #263238;
            color: #ECEFF1;
            padding: 14px;
            border-radius: 8px;
            font-family: 'Consolas', monospace;
            font-size: 13px;
            max-height: 300px;
            overflow-y: auto;
            margin: 16px 0;
        }
        .event-log .loading { color: #FFA726; }
        .event-log .interactive { color: #66BB6A; }
        .event-log .complete { color: #42A5F5; }
    </style>
</head>
<body>
    <h1>readystatechange 与加载状态追踪</h1>

    <div class="state-dashboard">
        <h2>document.readyState 状态仪表板</h2>
        <div class="state-indicator" id="state-loading">
            <div class="state-dot"></div>
            <span class="state-name">loading</span>
            <span class="state-desc">文档正在加载中</span>
            <span class="state-time" id="time-loading">-</span>
        </div>
        <div class="state-indicator" id="state-interactive">
            <div class="state-dot"></div>
            <span class="state-name">interactive</span>
            <span class="state-desc">文档解析完成,子资源可能仍在加载</span>
            <span class="state-time" id="time-interactive">-</span>
        </div>
        <div class="state-indicator" id="state-complete">
            <div class="state-dot"></div>
            <span class="state-name">complete</span>
            <span class="state-desc">文档和所有子资源加载完成</span>
            <span class="state-time" id="time-complete">-</span>
        </div>
    </div>

    <h2>事件触发日志</h2>
    <div class="event-log" id="eventLog"></div>

    <script>
        const eventLog = document.getElementById('eventLog');
        const startTime = performance.timeOrigin || 0;

        function logEvent(msg, type) {
            const elapsed = Math.round(performance.now());
            eventLog.innerHTML += '<div class="' + type + '">[' + elapsed + 'ms] ' + msg + '</div>';
            eventLog.scrollTop = eventLog.scrollHeight;
        }

        function updateState(stateName) {
            // 更新状态指示器
            document.querySelectorAll('.state-indicator').forEach(function(el) {
                el.classList.remove('current');
            });
            const stateEl = document.getElementById('state-' + stateName);
            if (stateEl) {
                stateEl.classList.add('active', 'current');
                document.getElementById('time-' + stateName).textContent =
                    Math.round(performance.now()) + 'ms';
            }
            // 之前的状态设为active但不current
            const states = ['loading', 'interactive', 'complete'];
            const currentIndex = states.indexOf(stateName);
            for (let i = 0; i < currentIndex; i++) {
                document.getElementById('state-' + states[i]).classList.add('active');
                document.getElementById('state-' + states[i]).classList.remove('current');
            }
        }

        // 检查当前状态(可能脚本执行时已经过了某个状态)
        logEvent('初始readyState: ' + document.readyState, document.readyState);

        if (document.readyState === 'loading') {
            updateState('loading');
        } else if (document.readyState === 'interactive') {
            updateState('loading');
            updateState('interactive');
        } else if (document.readyState === 'complete') {
            updateState('loading');
            updateState('interactive');
            updateState('complete');
        }

        // 监听readystatechange事件
        document.addEventListener('readystatechange', function() {
            const state = document.readyState;
            logEvent('readystatechange - ' + state, state);
            updateState(state);
        });

        // 监听DOMContentLoaded
        document.addEventListener('DOMContentLoaded', function() {
            logEvent('DOMContentLoaded 触发', 'interactive');
        });

        // 监听load
        window.addEventListener('load', function() {
            logEvent('window load 触发', 'complete');
        });

        // 监听beforeunload
        window.addEventListener('beforeunload', function(event) {
            logEvent('beforeunload 触发 - 页面即将卸载', 'loading');
        });
    </script>
</body>
</html>

浏览器兼容性

特性 Chrome Firefox Safari Edge IE
window.onload 全部 全部 全部 全部 全部
DOMContentLoaded 1+ 1+ 3.1+ 12+ 9+
readystatechange 1+ 1+ 1+ 12+ 4+
img.onload 1+ 1+ 1+ 12+ 6+
script.onload 2+ 1+ 3.1+ 12+ 不支持(IE9+)
link.onload 2+ 1+ 3.1+ 12+ 不支持(IE9+)
loading="lazy" 77+ 75+ 15.4+ 79+ 不支持
IntersectionObserver 51+ 55+ 12.1+ 15+ 不支持
performance.timing 6+ 7+ 8+ 12+ 9+
script defer 1+ 3.6+ 1+ 12+ 10+
script async 8+ 3.6+ 5.1+ 12+ 10+

IE兼容性说明:
• IE8及以下不支持DOMContentLoaded,可用readystatechange模拟
• IE8及以下script.onload不触发,需使用script.onreadystatechange
• IE9开始支持标准的DOMContentLoaded


注意事项与最佳实践

1. 优先使用DOMContentLoaded而非load

代码示例

// 不推荐:等待所有资源加载完成
window.onload = function() {
    initApp(); // 图片、CSS等都加载完才执行,可能很慢
};

// 推荐:DOM构建完成即可操作
document.addEventListener('DOMContentLoaded', function() {
    initApp(); // DOM可用就执行,更快响应
});

2. 处理DOMContentLoaded已触发的情况

代码示例

// 问题:脚本异步加载时,DOMContentLoaded可能已经触发
function onReady(callback) {
    if (document.readyState !== 'loading') {
        callback(); // DOM已就绪,直接执行
    } else {
        document.addEventListener('DOMContentLoaded', callback);
    }
}

onReady(function() {
    console.log('DOM已就绪,安全操作DOM');
});

3. 避免多个window.onload覆盖

代码示例

// 问题:后者覆盖前者
window.onload = handler1;
window.onload = handler2; // handler1被覆盖

// 解决方案:使用addEventListener
window.addEventListener('load', handler1);
window.addEventListener('load', handler2); // 两个都执行

4. 图片加载的错误处理

代码示例

const img = new Image();
img.onload = function() {
    console.log('图片加载成功');
};
img.onerror = function() {
    console.log('图片加载失败,使用占位图');
    img.src = 'placeholder.png';
};
img.src = 'photo.jpg';

5. 懒加载的最佳实践

代码示例

<!-- 推荐:原生loading="lazy" + 回退方案 -->
<img src="image.jpg" loading="lazy" alt="描述">

<!-- 带回退的完整方案 -->
<img src="image.jpg"
     loading="lazy"
     data-fallback="placeholder.jpg"
     onerror="if(this.dataset.fallback){this.src=this.dataset.fallback;delete this.dataset.fallback;}"
     alt="描述">

6. script标签放置位置

代码示例

<!-- 推荐:CSS在head,JS在body底部 -->
<head>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- 页面内容 -->
    <script src="app.js" defer></script>
</body>

代码规范示例

代码示例

/**
 * 页面加载管理器 - 统一管理加载生命周期
 */
class PageLoadManager {
    constructor() {
        this.callbacks = {
            domReady: [],
            loaded: [],
            beforeUnload: []
        };
        this.isDomReady = false;
        this.isLoaded = false;

        this._init();
    }

    _init() {
        // DOMContentLoaded
        if (document.readyState !== 'loading') {
            this._handleDomReady();
        } else {
            document.addEventListener('DOMContentLoaded', () => this._handleDomReady());
        }

        // load
        if (document.readyState === 'complete') {
            this._handleLoad();
        } else {
            window.addEventListener('load', () => this._handleLoad());
        }

        // beforeunload
        window.addEventListener('beforeunload', () => this._handleBeforeUnload());
    }

    _handleDomReady() {
        this.isDomReady = true;
        this.callbacks.domReady.forEach(cb => {
            try { cb(); } catch (e) { console.error('domReady callback error:', e); }
        });
        this.callbacks.domReady = [];
    }

    _handleLoad() {
        this.isLoaded = true;
        this.callbacks.loaded.forEach(cb => {
            try { cb(); } catch (e) { console.error('loaded callback error:', e); }
        });
        this.callbacks.loaded = [];
    }

    _handleBeforeUnload() {
        this.callbacks.beforeUnload.forEach(cb => {
            try { cb(); } catch (e) { console.error('beforeUnload callback error:', e); }
        });
    }

    /**
     * DOM构建完成后执行
     * @param {Function} callback
     */
    onDomReady(callback) {
        if (this.isDomReady) {
            callback();
        } else {
            this.callbacks.domReady.push(callback);
        }
        return this;
    }

    /**
     * 页面及所有资源加载完成后执行
     * @param {Function} callback
     */
    onLoaded(callback) {
        if (this.isLoaded) {
            callback();
        } else {
            this.callbacks.loaded.push(callback);
        }
        return this;
    }

    /**
     * 页面卸载前执行
     * @param {Function} callback
     */
    onBeforeUnload(callback) {
        this.callbacks.beforeUnload.push(callback);
        return this;
    }
}

// 使用示例
const pageLoad = new PageLoadManager();

pageLoad
    .onDomReady(function() {
        console.log('DOM已就绪,初始化应用');
    })
    .onLoaded(function() {
        console.log('页面完全加载,执行依赖资源的操作');
    })
    .onBeforeUnload(function() {
        console.log('页面即将卸载,保存数据');
    });

常见问题与解决方案

问题1:DOMContentLoaded不触发

代码示例

// 问题:使用了async脚本且脚本执行时间很长
// <script async src="slow-script.js"></script>
// async脚本不阻塞DOMContentLoaded,但如果脚本在DOMContentLoaded之前开始执行,
// 会阻塞DOM解析

// 解决方案:确保没有阻塞DOM解析的同步脚本
// 使用defer代替async(defer脚本在DOMContentLoaded之前按顺序执行)

问题2:图片onload不触发(缓存问题)

代码示例

// 问题:IE中已缓存的图片不触发onload
const img = new Image();
img.onload = function() { console.log('loaded'); };
img.src = 'cached-image.jpg'; // 如果已缓存,IE可能不触发onload

// 解决方案:先设置src再绑定onload,或检查complete属性
const img = new Image();
img.src = 'cached-image.jpg';
if (img.complete) {
    // 图片已缓存,直接处理
    handleImageLoad(img);
} else {
    img.onload = function() { handleImageLoad(img); };
}

问题3:iframe的load事件影响主页面

代码示例

// 问题:window.load等待所有iframe加载完成
// 如果iframe加载很慢,主页面load事件会延迟

// 解决方案1:动态加载iframe
// 不在HTML中写iframe,在load后动态创建

// 解决方案2:使用DOMContentLoaded代替load
document.addEventListener('DOMContentLoaded', function() {
    // 不等待iframe,DOM就绪即可
});

问题4:动态创建的script/link的加载检测

代码示例

// 动态创建的script标签
function loadScript(url) {
    return new Promise(function(resolve, reject) {
        const script = document.createElement('script');
        script.src = url;
        script.onload = function() { resolve(); };
        script.onerror = function() { reject(new Error('Failed to load: ' + url)); };
        document.head.appendChild(script);
    });
}

// 使用
loadScript('https://cdn.example.com/lib.js')
    .then(function() { console.log('脚本加载完成'); })
    .catch(function(err) { console.error(err); });

问题5:SPA中的load事件只触发一次

代码示例

// 问题:单页应用中路由切换不会触发load事件
// load事件只在页面首次加载时触发

// 解决方案:使用自定义事件或路由钩子
// Vue Router
router.afterEach(function() {
    window.dispatchEvent(new CustomEvent('routeChange'));
});

// 监听路由变化
window.addEventListener('routeChange', function() {
    console.log('路由变化,重新初始化');
});

总结

页面加载事件是Web应用生命周期的基石,掌握其细节对于构建高性能应用至关重要:

  • DOMContentLoaded vs load:DOMContentLoaded在DOM树构建完成时触发,不等待CSS和图片;load在所有资源加载完成时触发。大多数初始化操作应使用DOMContentLoaded。

  • readystatechange:提供更细粒度的加载状态追踪,interactive对应DOMContentLoaded,complete对应load。可用于兼容不支持DOMContentLoaded的旧浏览器。

  • 资源加载事件:图片、脚本、样式表都有独立的onload和onerror事件,可用于追踪单个资源的加载状态,实现加载进度指示和错误处理。

  • 性能优化:CSS放head、JS用defer、使用preload/prefetch预加载关键资源、图片使用loading="lazy"懒加载,这些策略可以显著提升页面加载性能。

  • 懒加载:原生loading="lazy"是最简单的方案,IntersectionObserver提供更灵活的控制,scroll事件+getBoundingClientRect是兼容性最好的回退方案。

  • 加载状态检测:始终检查document.readyState以处理脚本异步加载时DOMContentLoaded已触发的情况,避免回调永远不会执行的问题。

常见问题

1. DOMContentLoaded不触发怎么办?

如果使用了async脚本且脚本执行时间很长,可能会阻塞DOM解析。解决方案是确保没有阻塞DOM解析的同步脚本,使用defer代替async(defer脚本在DOMContentLoaded之前按顺序执行)。

2. 图片onload不触发(缓存问题)如何解决?

在IE中,已缓存的图片可能不触发onload事件。解决方案是先设置src再绑定onload,或检查complete属性:if (img.complete) { handleImageLoad(img); } else { img.onload = function() { handleImageLoad(img); }; }

3. iframe的load事件影响主页面怎么办?

window.load等待所有iframe加载完成,如果iframe加载很慢,主页面load事件会延迟。解决方案有两种:一是动态加载iframe(不在HTML中写iframe,在load后动态创建);二是使用DOMContentLoaded代替load,不等待iframe,DOM就绪即可执行。

4. 如何检测动态创建的script/link是否加载完成?

动态创建的script标签可以使用Promise包装加载检测:function loadScript(url) { return new Promise(function(resolve, reject) { const script = document.createElement('script'); script.src = url; script.onload = function() { resolve(); }; script.onerror = function() { reject(new Error('Failed to load: ' + url)); }; document.head.appendChild(script); }); }

5. SPA中load事件只触发一次如何处理?

单页应用中路由切换不会触发load事件,load事件只在页面首次加载时触发。解决方案是使用自定义事件或路由钩子,例如Vue Router的router.afterEach钩子中派发routeChange自定义事件,然后监听该事件来重新初始化页面。

标签: HTML onload DOMContentLoaded 页面加载事件 资源加载 懒加载 性能优化 readystatechange script defer

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: JS集成:HTML onclick事件: - 详细教程与实战指南 下一篇: JS集成:HTML onsubmit事件 - 详细教程与实战指南

poll相关推荐