一次搞懂 script标签 async、defer、link标签 preload和prefetch属性

403 阅读7分钟

Script加载属性与资源预加载详解

我将详细解释asyncdeferpreloadprefetch属性,并通过一个交互式示例展示它们的工作原理。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Script加载属性与资源预加载详解</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
            min-height: 100vh;
            padding: 40px 20px;
            color: #333;
            line-height: 1.6;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            box-shadow: 0 15px 50px rgba(0, 0, 0, 0.2);
            overflow: hidden;
        }
        
        header {
            background: #4e54c8;
            color: white;
            padding: 40px;
            text-align: center;
            position: relative;
        }
        
        h1 {
            font-size: 2.8rem;
            margin-bottom: 15px;
            text-shadow: 0 2px 4px rgba(0,0,0,0.3);
        }
        
        .subtitle {
            font-size: 1.3rem;
            max-width: 800px;
            margin: 0 auto;
            opacity: 0.9;
        }
        
        .content {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
            gap: 30px;
            padding: 40px;
        }
        
        .section {
            background: #f8f9fa;
            border-radius: 15px;
            padding: 30px;
            box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05);
            transition: transform 0.3s ease;
        }
        
        .section:hover {
            transform: translateY(-5px);
        }
        
        h2 {
            color: #4e54c8;
            margin-bottom: 20px;
            padding-bottom: 15px;
            border-bottom: 2px solid #e9ecef;
            font-size: 1.8rem;
        }
        
        .attribute-card {
            background: white;
            border-radius: 10px;
            padding: 25px;
            margin-bottom: 25px;
            box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05);
            border-left: 4px solid;
        }
        
        .defer-card { border-left-color: #4e54c8; }
        .async-card { border-left-color: #fdbb2d; }
        .preload-card { border-left-color: #b21f1f; }
        .prefetch-card { border-left-color: #1a2a6c; }
        
        .attribute-title {
            display: flex;
            align-items: center;
            margin-bottom: 15px;
        }
        
        .attribute-name {
            font-size: 1.5rem;
            font-weight: bold;
            margin-right: 15px;
        }
        
        .attribute-tag {
            background: #e9ecef;
            padding: 5px 10px;
            border-radius: 5px;
            font-family: monospace;
            font-size: 0.9rem;
        }
        
        .attribute-description {
            margin-bottom: 15px;
            font-size: 1.1rem;
        }
        
        .timeline {
            background: #e9ecef;
            height: 10px;
            border-radius: 5px;
            margin: 20px 0;
            position: relative;
            overflow: hidden;
        }
        
        .timeline-event {
            position: absolute;
            top: 0;
            height: 100%;
            width: 0;
            background: #4e54c8;
            border-radius: 5px;
            transition: width 1.5s ease-out;
        }
        
        .timeline-labels {
            display: flex;
            justify-content: space-between;
            margin-top: 10px;
            font-size: 0.9rem;
            color: #6c757d;
        }
        
        .comparison-table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        
        .comparison-table th, .comparison-table td {
            padding: 15px;
            text-align: left;
            border-bottom: 1px solid #dee2e6;
        }
        
        .comparison-table th {
            background: #4e54c8;
            color: white;
            font-weight: normal;
        }
        
        .comparison-table tr:nth-child(even) {
            background: #f8f9fa;
        }
        
        .controls {
            display: flex;
            gap: 15px;
            margin-top: 20px;
            flex-wrap: wrap;
        }
        
        button {
            padding: 14px 28px;
            border: none;
            border-radius: 8px;
            background: #4e54c8;
            color: white;
            font-size: 1rem;
            font-weight: bold;
            cursor: pointer;
            transition: all 0.3s ease;
            flex: 1;
            min-width: 200px;
        }
        
        button:hover {
            background: #3f43a8;
            transform: translateY(-3px);
            box-shadow: 0 7px 15px rgba(0,0,0,0.1);
        }
        
        .demo-section {
            background: #e9ecef;
            padding: 25px;
            border-radius: 10px;
            margin-top: 20px;
        }
        
        .demo-output {
            background: white;
            padding: 20px;
            border-radius: 8px;
            min-height: 100px;
            font-family: monospace;
            white-space: pre-wrap;
            margin-top: 15px;
            box-shadow: inset 0 2px 5px rgba(0,0,0,0.05);
        }
        
        .key-points {
            background: #eef7ff;
            border-left: 4px solid #4e54c8;
            padding: 20px;
            margin: 25px 0;
            border-radius: 0 8px 8px 0;
        }
        
        .key-points h3 {
            color: #4e54c8;
            margin-bottom: 15px;
        }
        
        .key-points ul {
            padding-left: 25px;
        }
        
        .key-points li {
            margin-bottom: 10px;
        }
        
        @media (max-width: 768px) {
            .content {
                grid-template-columns: 1fr;
                padding: 20px;
            }
            
            header {
                padding: 30px 20px;
            }
            
            h1 {
                font-size: 2.2rem;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>Script加载与资源预加载详解</h1>
            <p class="subtitle">深入理解 async、defer、preload 和 prefetch 的工作原理与应用场景</p>
        </header>
        
        <div class="content">
            <div class="section">
                <h2>Script 加载属性</h2>
                
                <div class="attribute-card defer-card">
                    <div class="attribute-title">
                        <div class="attribute-name">defer</div>
                        <div class="attribute-tag">&lt;script defer&gt;</div>
                    </div>
                    <div class="attribute-description">
                        延迟执行脚本。脚本会在HTML解析完成后、DOMContentLoaded事件前执行,按顺序执行。
                    </div>
                    <div class="timeline">
                        <div class="timeline-event" id="defer-timeline" style="width: 0%"></div>
                    </div>
                    <div class="timeline-labels">
                        <span>HTML解析开始</span>
                        <span>DOMContentLoaded</span>
                    </div>
                    <div class="key-points">
                        <h3>关键特性</h3>
                        <ul>
                            <li>不阻塞HTML解析</li>
                            <li>按脚本在文档中的顺序执行</li>
                            <li>在DOMContentLoaded事件前执行</li>
                            <li>适用于依赖DOM的脚本</li>
                        </ul>
                    </div>
                </div>
                
                <div class="attribute-card async-card">
                    <div class="attribute-title">
                        <div class="attribute-name">async</div>
                        <div class="attribute-tag">&lt;script async&gt;</div>
                    </div>
                    <div class="attribute-description">
                        异步加载脚本。脚本下载不阻塞HTML解析,下载完成后立即执行(可能在HTML解析完成前)。
                    </div>
                    <div class="timeline">
                        <div class="timeline-event" id="async-timeline" style="width: 0%"></div>
                    </div>
                    <div class="timeline-labels">
                        <span>HTML解析开始</span>
                        <span>脚本下载完成立即执行</span>
                    </div>
                    <div class="key-points">
                        <h3>关键特性</h3>
                        <ul>
                            <li>下载不阻塞HTML解析</li>
                            <li>执行顺序无法保证(先下载完先执行)</li>
                            <li>适用于独立脚本(如分析统计)</li>
                            <li>可能阻塞DOMContentLoaded事件</li>
                        </ul>
                    </div>
                </div>
                
                <div class="demo-section">
                    <h3>加载行为演示</h3>
                    <div class="controls">
                        <button id="normal-btn">普通Script加载</button>
                        <button id="defer-btn">defer加载</button>
                        <button id="async-btn">async加载</button>
                    </div>
                    <div class="demo-output" id="script-output"></div>
                </div>
            </div>
            
            <div class="section">
                <h2>资源预加载属性</h2>
                
                <div class="attribute-card preload-card">
                    <div class="attribute-title">
                        <div class="attribute-name">preload</div>
                        <div class="attribute-tag">&lt;link rel="preload"&gt;</div>
                    </div>
                    <div class="attribute-description">
                        指示浏览器立即预加载指定资源,该资源在当前导航中肯定会用到。
                    </div>
                    <div class="key-points">
                        <h3>关键特性</h3>
                        <ul>
                            <li>高优先级加载</li>
                            <li>用于当前页面必定需要的资源</li>
                            <li>需指定as属性(如as="script"、as="style")</li>
                            <li>可搭配crossorigin属性</li>
                        </ul>
                    </div>
                </div>
                
                <div class="attribute-card prefetch-card">
                    <div class="attribute-title">
                        <div class="attribute-name">prefetch</div>
                        <div class="attribute-tag">&lt;link rel="prefetch"&gt;</div>
                    </div>
                    <div class="attribute-description">
                        指示浏览器在空闲时预取资源,该资源可能在后续导航中使用。
                    </div>
                    <div class="key-points">
                        <h3>关键特性</h3>
                        <ul>
                            <li>低优先级加载</li>
                            <li>用于未来可能需要的资源</li>
                            <li>不会阻塞当前页面关键资源</li>
                            <li>适用于预加载下一页面的资源</li>
                        </ul>
                    </div>
                </div>
                
                <table class="comparison-table">
                    <thead>
                        <tr>
                            <th>属性</th>
                            <th>加载优先级</th>
                            <th>使用时机</th>
                            <th>适用资源</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td><strong>preload</strong></td>
                            <td>高(立即加载)</td>
                            <td>当前页面</td>
                            <td>字体、关键CSS/JS、首屏图片</td>
                        </tr>
                        <tr>
                            <td><strong>prefetch</strong></td>
                            <td>低(空闲时加载)</td>
                            <td>未来页面</td>
                            <td>下一页面的资源、非关键资源</td>
                        </tr>
                    </tbody>
                </table>
                
                <div class="demo-section">
                    <h3>预加载演示</h3>
                    <div class="controls">
                        <button id="preload-btn">预加载资源 (preload)</button>
                        <button id="prefetch-btn">预取资源 (prefetch)</button>
                    </div>
                    <div class="demo-output" id="preload-output"></div>
                </div>
                
                <div class="key-points">
                    <h3>使用建议</h3>
                    <ul>
                        <li><strong>关键JS</strong>:使用defer加载(保持顺序)</li>
                        <li><strong>独立JS</strong>:使用async加载(如分析脚本)</li>
                        <li><strong>关键CSS/字体</strong>:使用preload预加载</li>
                        <li><strong>下一页资源</strong>:使用prefetch预取</li>
                        <li>避免同时使用async和defer</li>
                    </ul>
                </div>
            </div>
        </div>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const scriptOutput = document.getElementById('script-output');
            const preloadOutput = document.getElementById('preload-output');
            
            // 模拟脚本加载
            function simulateScriptLoad(type) {
                scriptOutput.textContent = '';
                scriptOutput.textContent += `开始加载${type}脚本...\n`;
                
                // 模拟HTML解析过程
                setTimeout(() => {
                    scriptOutput.textContent += 'HTML解析中...\n';
                    
                    if (type === '普通') {
                        scriptOutput.textContent += '脚本加载阻塞HTML解析...\n';
                        
                        setTimeout(() => {
                            scriptOutput.textContent += '脚本加载完成,执行脚本\n';
                            scriptOutput.textContent += '继续HTML解析...\n';
                            scriptOutput.textContent += 'DOMContentLoaded事件触发\n';
                        }, 1000);
                    } 
                    else if (type === 'defer') {
                        setTimeout(() => {
                            scriptOutput.textContent += 'HTML解析完成\n';
                            scriptOutput.textContent += '执行defer脚本\n';
                            scriptOutput.textContent += 'DOMContentLoaded事件触发\n';
                        }, 1000);
                    }
                    else if (type === 'async') {
                        setTimeout(() => {
                            scriptOutput.textContent += 'async脚本加载完成,立即执行(可能中断HTML解析)\n';
                            
                            setTimeout(() => {
                                scriptOutput.textContent += 'HTML解析完成\n';
                                scriptOutput.textContent += 'DOMContentLoaded事件触发\n';
                            }, 500);
                        }, 800);
                    }
                }, 300);
            }
            
            // 预加载演示
            function simulatePreload(type) {
                preloadOutput.textContent = '';
                
                if (type === 'preload') {
                    preloadOutput.textContent += '开始预加载关键资源...\n';
                    preloadOutput.textContent += '资源优先级: 高\n';
                    
                    setTimeout(() => {
                        preloadOutput.textContent += '资源已预加载完成\n';
                        preloadOutput.textContent += '当页面需要该资源时,可立即使用\n';
                    }, 800);
                }
                else if (type === 'prefetch') {
                    preloadOutput.textContent += '开始预取未来可能需要的资源...\n';
                    preloadOutput.textContent += '资源优先级: 低(浏览器空闲时加载)\n';
                    
                    setTimeout(() => {
                        preloadOutput.textContent += '资源预取中...\n';
                        
                        setTimeout(() => {
                            preloadOutput.textContent += '资源预取完成\n';
                            preloadOutput.textContent += '该资源将在后续导航中使用\n';
                        }, 1200);
                    }, 500);
                }
            }
            
            // 时间轴动画
            function animateTimeline(elementId, duration) {
                const timeline = document.getElementById(elementId);
                timeline.style.width = '0%';
                
                setTimeout(() => {
                    timeline.style.transition = `width ${duration}ms ease-out`;
                    timeline.style.width = '100%';
                }, 100);
            }
            
            // 绑定按钮事件
            document.getElementById('normal-btn').addEventListener('click', () => {
                simulateScriptLoad('普通');
                animateTimeline('defer-timeline', 2000);
                animateTimeline('async-timeline', 2000);
            });
            
            document.getElementById('defer-btn').addEventListener('click', () => {
                simulateScriptLoad('defer');
                animateTimeline('defer-timeline', 2000);
            });
            
            document.getElementById('async-btn').addEventListener('click', () => {
                simulateScriptLoad('async');
                animateTimeline('async-timeline', 1500);
            });
            
            document.getElementById('preload-btn').addEventListener('click', () => {
                simulatePreload('preload');
            });
            
            document.getElementById('prefetch-btn').addEventListener('click', () => {
                simulatePreload('prefetch');
            });
        });
    </script>
</body>
</html>

详细解释

1. Script 标签属性

async 属性
  • 作用:异步加载脚本
  • 行为
    • 下载不阻塞HTML解析
    • 脚本下载完成后立即执行(可能在HTML解析完成前)
    • 多个async脚本执行顺序无法保证(先下载完先执行)
  • 适用场景:独立脚本(如分析统计、广告脚本)
  • 语法
    <script async src="script.js"></script>
    
defer 属性
  • 作用:延迟执行脚本
  • 行为
    • 下载不阻塞HTML解析
    • 在HTML解析完成后、DOMContentLoaded事件前执行
    • 多个defer脚本按它们在文档中的顺序执行
  • 适用场景:依赖DOM的脚本,需要按顺序执行的脚本
  • 语法
    <script defer src="script.js"></script>
    

2. 资源预加载属性

preload 属性
  • 作用:立即预加载当前页面关键资源
  • 特点
    • 高优先级加载
    • 必须指定as属性(如scriptstylefont
    • 当前页面一定会使用的资源
  • 语法
    <link rel="preload" href="font.woff2" as="font" crossorigin>
    
  • 适用场景
    • 关键字体文件
    • 首屏关键CSS/JS
    • 首屏大图
prefetch 属性
  • 作用:在浏览器空闲时预取未来可能需要的资源
  • 特点
    • 低优先级加载
    • 不会阻塞当前页面关键资源
    • 适用于后续导航可能需要的资源
  • 语法
    <link rel="prefetch" href="next-page.js" as="script">
    
  • 适用场景
    • 预加载下一页面的资源
    • 预加载非关键资源(如图标、背景图)

3. 对比总结

属性加载时机执行/使用时机优先级顺序保证
async立即下载下载完成后立即执行❌ 无保证
defer立即下载HTML解析完成后执行✅ 有保证
preload立即下载当前页面需要时使用最高-
prefetch浏览器空闲时下载后续页面需要时使用最低-

4. 最佳实践建议

  1. 关键JS:使用defer加载(保持执行顺序)
  2. 独立JS:使用async加载(如分析脚本)
  3. 关键CSS/字体:使用preload预加载
  4. 下一页资源:使用prefetch预取
  5. 避免混用:不要同时使用asyncdefer属性
  6. 谨慎使用preload只用于关键资源,滥用会浪费带宽

通过合理使用这些属性,可以显著优化页面加载性能,提升用户体验。