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 loadDOMContentLoaded在DOM树构建完成时触发,不等待CSS和图片;load在所有资源加载完成时触发。大多数初始化操作应使用DOMContentLoaded

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

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

  • 性能优化: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相关推荐