您当前的位置:首页 > 计算机 > 编程开发 > Html+Div+Css(前端)

前端新人必看:搞懂 script 的 async 与 defer,页面加载快如闪电(含金科玉律)

时间:02-09来源:作者:点击数:

引言:为什么你的网页总在"转圈"?

还记得第一次把个人博客部署到线上时的那种兴奋吗?我兴冲冲地把地址甩到群里,结果三秒后就有小伙伴吐槽:"你这网站是拨号上网时代的遗物吗?加载个按钮都要转三圈半。"那一刻,我恨不得把电脑塞进时光机送回1998年。

其实很多时候,网页加载慢并不是因为你的代码写得有多烂(虽然也不排除这种可能),而是因为那些看似无害的<script>标签在暗地里搞事情。它们就像一群不请自来的亲戚,非得在主人(HTML解析器)忙得不可开交的时候插嘴,把整个家(页面加载流程)搅得鸡飞狗跳。

script 标签的默认行为有多"霸道"?

让我们先做个小实验。假设你有这么个页面:

<!DOCTYPE html>
<html>
<head>
    <title>我的超酷网站</title>
    <script src="https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js"></script>
</head>
<body>
    <h1>欢迎光临</h1>
    <p>这句话要等到上面那个2.8KB的lodash加载完才能看到</p>
</body>
</html>

浏览器看到这个页面时的内心OS大概是这样的:

“好的,开始解析HTML…哦,这里有个script标签!等等,我得先去下载这个lodash.js,下载完了还要执行它。什么?你说后面还有内容?不好意思,我得等这个大爷伺候完了再说。”

这就是script标签的默认行为——完全阻塞。它不仅会暂停HTML解析,还会暂停其他资源的下载。就像一个霸道的出租车司机,非得等到自己吃饱喝足了才肯发车。

浏览器解析 HTML 遇到 script 时到底发生了什么?

让我们用一段代码来重现这个"惨案":

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>阻塞演示</title>
</head>
<body>
    <div id="content"></div>
    
    <!-- 故意放一个超大脚本 -->
    <script>
        // 模拟一个耗时5秒的计算
        const start = Date.now();
        while(Date.now() - start < 5000) {
            // 啥也不干,就是硬等
        }
        document.getElementById('content').innerHTML = '终于加载完了!';
    </script>
    
    <!-- 这个div要5秒后才会出现 -->
    <div>我是无辜的路人甲,为什么要陪我等?</div>
</body>
</html>

打开这个页面,你会发现整个页面空白了5秒,然后所有内容才"唰"地一下出现。这就是阻塞的代价——用户看到的不是渐进增强的页面,而是一个尴尬的白屏。

揭开 async 和 defer 的神秘面纱

它们不是魔法,但能让你的页面起飞

async和defer就像是给script标签请的两个保镖,专门负责管教那些不听话的脚本。它们的核心理念很简单:让脚本下载不耽误正事(HTML解析),至于执行时机,各有一套规矩

从阻塞到非阻塞:一场加载策略的进化史

早期的网页很简单,几个脚本标签,一些内联代码,大家排队执行,相安无事。但随着网页越来越复杂,脚本越来越多,这种"排队打饭"的模式就显得效率低下了。

于是浏览器厂商们开始琢磨:能不能让脚本在后台偷偷下载,别耽误HTML解析的正事?这就是async和defer的由来。它们就像是脚本界的"特殊通道",让不同类型的脚本能够更高效地加载。

深入剖析 async 的工作方式

脚本下载不阻塞 HTML 解析,但执行会打断一切

async就像是个急性子的快递员,包裹(脚本)一到就立马派送(执行),完全不管你现在是不是在忙。让我们看个实际的例子:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>async 演示</title>
</head>
<body>
    <h1>页面开始加载</h1>
    <div id="log"></div>
    
    <script>
        // 记录日志的函数
        function log(msg) {
            const div = document.getElementById('log');
            div.innerHTML += `<p>${new Date().toLocaleTimeString()}: ${msg}</p>`;
        }
        
        log('HTML 解析开始');
    </script>
    
    <!-- 第一个 async 脚本 -->
    <script async src="https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js" 
            onload="log('Lodash 加载完成')"></script>
    
    <script>
        log('继续解析 HTML');
    </script>
    
    <!-- 第二个 async 脚本 -->
    <script async src="https://cdn.jsdelivr.net/npm/moment@2/moment.min.js" 
            onload="log('Moment 加载完成')"></script>
    
    <script>
        log('HTML 解析完成');
    </script>
</body>
</html>

在这个例子中,你会发现:

  1. HTML解析不会因为脚本下载而暂停
  2. 但脚本一旦下载完成,会立即执行,可能打断HTML解析
  3. 两个async脚本的执行顺序不确定(看谁先下载完)
适合谁用?独立、无依赖的第三方脚本最爱它

async特别适合那些不依赖其他脚本,也不被其他脚本依赖的独立模块。比如:

<!-- 广告代码 - 完美的 async 候选人 -->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>

<!-- 统计代码 - 另一个 async 爱好者 -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>

<!-- 独立的小工具 - 也很适合 -->
<script async>
    // 一个简单的返回顶部按钮,不需要任何依赖
    window.addEventListener('load', function() {
        const btn = document.createElement('button');
        btn.innerHTML = '↑';
        btn.style.cssText = 'position:fixed;bottom:20px;right:20px;';
        btn.onclick = () => window.scrollTo({top: 0, behavior: 'smooth'});
        document.body.appendChild(btn);
    });
</script>

defer 是怎么做到"先解析后执行"的?

所有 defer 脚本按顺序排队,等 DOM 准备好才上场

defer就像是剧院里彬彬有礼的观众,等到所有演员(HTML)都就位了,才按票号(出现顺序)依次上场。让我们见证一下它的优雅:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>defer 演示</title>
    
    <!-- 三个 defer 脚本,注意顺序 -->
    <script defer>
        console.log('defer 脚本 1: 我在 DOMContentLoaded 之前执行');
        console.log('此时 DOM 是否加载完成?', document.readyState);
    </script>
    
    <script defer src="https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js"></script>
    
    <script defer>
        console.log('defer 脚本 2: 我在 lodash 之后执行,因为我在后面');
        console.log('lodash 可用吗?', typeof _ !== 'undefined');
    </script>
</head>
<body>
    <h1>页面内容</h1>
    <p>看控制台输出顺序</p>
    
    <script>
        // 这个普通脚本会先执行
        console.log('普通脚本:我最先执行');
    </script>
</body>
</html>

输出顺序总是:

  1. 普通脚本:我最先执行
  2. defer 脚本 1: 我在 DOMContentLoaded 之前执行
  3. defer 脚本 2: 我在 lodash 之后执行,因为我在后面
为什么它是模块化 JS 的最佳拍档?

defer的这种"按顺序延迟执行"的特性,让它成为了模块化代码的完美搭档。想象一下你在写一个复杂的单页应用:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>模块化加载演示</title>
    
    <!-- 基础库,先加载 -->
    <script defer src="js/utils.js"></script>
    
    <!-- 依赖基础库的功能模块 -->
    <script defer src="js/api.js"></script>
    <script defer src="js/router.js"></script>
    
    <!-- 最后加载主应用 -->
    <script defer src="js/app.js"></script>
</head>
<body>
    <div id="app">Loading...</div>
</body>
</html>

对应的模块代码:

// utils.js - 基础工具
window.MyApp = window.MyApp || {};

MyApp.utils = {
    log: (msg) => console.log(`[MyApp] ${msg}`),
    formatDate: (date) => date.toLocaleDateString()
};

// api.js - API 模块,依赖 utils
MyApp.api = {
    fetchData: async function(endpoint) {
        MyApp.utils.log(`Fetching data from ${endpoint}`);
        // 模拟 API 调用
        return new Promise(resolve => {
            setTimeout(() => resolve({data: 'ok'}), 100);
        });
    }
};

// router.js - 路由模块
MyApp.router = {
    routes: {},
    addRoute: function(path, handler) {
        this.routes[path] = handler;
    },
    navigate: function(path) {
        MyApp.utils.log(`Navigating to ${path}`);
        if (this.routes[path]) {
            this.routes[path]();
        }
    }
};

// app.js - 主应用,依赖所有前面的模块
document.addEventListener('DOMContentLoaded', function() {
    MyApp.utils.log('应用启动中...');
    
    // 配置路由
    MyApp.router.addRoute('/', () => {
        document.getElementById('app').innerHTML = '<h1>首页</h1>';
    });
    
    MyApp.router.addRoute('/about', () => {
        document.getElementById('app').innerHTML = '<h1>关于</h1>';
    });
    
    // 启动应用
    MyApp.router.navigate('/');
});

由于defer保证了执行顺序,我们不用担心依赖问题。即使这些脚本在HTML解析完成前就已经下载好了,它们也会耐心地排队等待,按照正确的顺序执行。

async vs defer:不只是"谁快谁慢"那么简单

执行时机对比:谁先谁后,谁乱谁稳?

让我们用一个综合示例来彻底搞清楚它们的区别:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>async vs defer 大对决</title>
    
    <script>
        // 记录各种事件的时间点
        window.logTimes = {
            scripts: [],
            events: []
        };
        
        function addScriptLog(type, name) {
            logTimes.scripts.push({
                type: type,
                name: name,
                time: performance.now()
            });
        }
        
        function addEventLog(event) {
            logTimes.events.push({
                event: event,
                time: performance.now()
            });
        }
    </script>
    
    <!-- 各种脚本混用 -->
    <script defer onload="addScriptLog('defer', 'defer1')">/* defer 1 */</script>
    <script async onload="addScriptLog('async', 'async1')">/* async 1 */</script>
    <script onload="addScriptLog('normal', 'normal1')">/* normal 1 */</script>
    <script defer onload="addScriptLog('defer', 'defer2')">/* defer 2 */</script>
    <script async onload="addScriptLog('async', 'async2')">/* async 2 */</script>
    
    <script>
        // 监听关键事件
        document.addEventListener('DOMContentLoaded', () => addEventLog('DOMContentLoaded'));
        window.addEventListener('load', () => addEventLog('load'));
        
        // 页面加载完成后显示结果
        window.addEventListener('load', function() {
            setTimeout(() => {
                console.table(logTimes.scripts);
                console.table(logTimes.events);
                
                // 分析结果
                const analysis = `
                执行顺序分析:
                1. normal1 - 立即执行,阻塞解析
                2. async1 或 async2 - 谁先下载完谁先执行
                3. defer1 - DOMContentLoaded 前执行
                4. defer2 - DOMContentLoaded 前执行(在 defer1 后)
                
                事件触发:
                - DOMContentLoaded:在 defer 脚本执行后触发
                - load:所有资源加载完后触发
                `;
                console.log(analysis);
            }, 100);
        });
    </script>
</head>
<body>
    <h1>查看控制台输出</h1>
</body>
</html>
依赖关系处理能力大比拼

async和defer在处理依赖关系时的表现截然不同:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>依赖关系测试</title>
</head>
<body>
    <h1>依赖关系演示</h1>
    
    <!-- 场景1:async 脚本有依赖 -->
    <h2>async 依赖测试(可能出错)</h2>
    <script>
        // 定义一个基础对象
        window.MyLibrary = {
            version: '1.0.0'
        };
    </script>
    
    <!-- 这个 async 脚本依赖 MyLibrary -->
    <script async>
        // 这段代码可能会报错,因为执行时机不确定
        if (window.MyLibrary) {
            console.log('async: MyLibrary 版本是', MyLibrary.version);
        } else {
            console.error('async: MyLibrary 还没准备好!');
        }
    </script>
    
    <!-- 场景2:defer 脚本有依赖 -->
    <h2>defer 依赖测试(安全可靠)</h2>
    <script>
        // 定义另一个基础对象
        window.AnotherLib = {
            utils: {
                format: (str) => str.toUpperCase()
            }
        };
    </script>
    
    <script defer>
        // 这个一定能正常工作,因为 defer 保证了执行顺序
        console.log('defer: 格式化结果', AnotherLib.utils.format('hello'));
    </script>
    
    <!-- 场景3:混合使用的复杂情况 -->
    <h2>复杂依赖场景</h2>
    <script>
        // 主库
        window.Framework = {
            components: {},
            register: function(name, component) {
                this.components[name] = component;
            }
        };
    </script>
    
    <!-- 组件定义(用 defer,确保在 Framework 后执行) -->
    <script defer>
        Framework.register('button', {
            template: '<button>Click me</button>'
        });
    </script>
    
    <!-- 应用初始化(用 defer,确保在所有组件后执行) -->
    <script defer>
        document.addEventListener('DOMContentLoaded', function() {
            console.log('可用组件:', Object.keys(Framework.components));
        });
    </script>
</body>
</html>
对 DOMContentLoaded 和 load 事件的影响差异

让我们深入研究这两种属性对关键事件的影响:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>事件时机测试</title>
    
    <script>
        const eventLog = [];
        
        function logEvent(event, detail) {
            const entry = {
                time: performance.now().toFixed(2),
                event: event,
                detail: detail || ''
            };
            eventLog.push(entry);
            console.table([entry]);
        }
        
        // 监听所有相关事件
        document.addEventListener('readystatechange', () => {
            logEvent('readystatechange', document.readyState);
        });
        
        document.addEventListener('DOMContentLoaded', () => {
            logEvent('DOMContentLoaded');
        });
        
        window.addEventListener('load', () => {
            logEvent('load');
        });
    </script>
    
    <!-- 测试各种脚本对事件的影响 -->
    <script defer 
            src="data:application/javascript,console.log('defer script executed')">
    </script>
    
    <script async 
            src="data:application/javascript,console.log('async script executed')">
    </script>
    
    <script>
        logEvent('inline script');
    </script>
</head>
<body>
    <h1>事件时机测试结果</h1>
    <p>查看控制台了解详细事件流</p>
    
    <script>
        // 页面加载完成后显示总结
        window.addEventListener('load', () => {
            setTimeout(() => {
                console.log('%c事件流总结', 'font-size: 16px; font-weight: bold;');
                console.table(eventLog);
                
                const summary = `
                关键观察点:
                1. defer 脚本在 DOMContentLoaded 之前执行
                2. async 脚本可能在 DOMContentLoaded 之前或之后执行
                3. 所有脚本都在 load 事件之前执行
                4. readystatechange 会经历: loading -> interactive -> complete
                `;
                console.log(summary);
            }, 100);
        });
    </script>
</body>
</html>

真实开发场景中的选择指南

广告脚本、统计代码该用 async 还是 defer?

在实际项目中,不同类型的第三方代码有不同的最佳实践:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>第三方脚本最佳实践</title>
    
    <!-- 广告代码 - 用 async,不阻塞页面展示 -->
    <!-- Google AdSense -->
    <script async 
            src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"
            onerror="handleAdError()">
    </script>
    
    <!-- 统计代码 - 用 async,数据收集不能影响主流程 -->
    <!-- Google Analytics -->
    <script async 
            src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID">
    </script>
    <script async>
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', 'GA_MEASUREMENT_ID');
    </script>
    
    <!-- 客服聊天 - 可以 defer,等页面稳定后再加载 -->
    <script defer>
        // 等页面加载完成后再初始化客服系统
        window.addEventListener('load', function() {
            // 加载客服脚本
            const script = document.createElement('script');
            script.src = 'https://customer-service-provider.com/chat.js';
            document.body.appendChild(script);
        });
    </script>
    
    <!-- A/B测试 - 用 async,快速生效 -->
    <script async>
        // 简单的 A/B 测试实现
        (function() {
            const variant = Math.random() > 0.5 ? 'A' : 'B';
            localStorage.setItem('ab_variant', variant);
            
            // 应用变体
            if (variant === 'B') {
                document.documentElement.classList.add('variant-b');
            }
        })();
    </script>
</head>
<body>
    <h1>第三方脚本集成示例</h1>
    
    <!-- 广告位 -->
    <ins class="adsbygoogle"
         style="display:block"
         data-ad-client="ca-pub-1234567890"
         data-ad-slot="1234567890"
         data-ad-format="auto">
    </ins>
    
    <script>
        // 处理广告加载失败
        function handleAdError() {
            console.warn('广告加载失败,显示占位内容');
            const adSlots = document.querySelectorAll('.adsbygoogle');
            adSlots.forEach(slot => {
                slot.innerHTML = '<p style="background:#f0f0f0;padding:20px;text-align:center;">广告位</p>';
            });
        }
        
        // 初始化广告(如果还没自动初始化)
        if (typeof adsbygoogle !== 'undefined') {
            adsbygoogle.push({});
        }
    </script>
</body>
</html>
SPA 应用中如何合理安排入口脚本?

单页应用的脚本加载策略更加复杂,需要精心安排:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>SPA 脚本加载策略</title>
    
    <!-- 关键样式内联,避免闪屏 -->
    <style>
        /* 关键 CSS 直接内联 */
        body { margin: 0; font-family: sans-serif; }
        #app { min-height: 100vh; }
        .loading { text-align: center; padding: 50px; }
    </style>
    
    <!-- 第一步:加载应用核心(用 defer,确保顺序) -->
    <script defer src="js/core/polyfills.js"></script>
    <script defer src="js/core/framework.js"></script>
    <script defer src="js/core/router.js"></script>
    
    <!-- 第二步:加载功能模块(用 defer) -->
    <script defer src="js/modules/auth.js"></script>
    <script defer src="js/modules/api.js"></script>
    <script defer src="js/modules/store.js"></script>
    
    <!-- 第三步:主应用入口(用 defer,确保所有依赖加载完) -->
    <script defer src="js/app.js"></script>
    
    <!-- 非关键功能可以 async 加载 -->
    <script async src="js/features/analytics.js"></script>
    <script async src="js/features/chat-widget.js"></script>
    
    <!-- 预加载关键资源 -->
    <link rel="preload" href="js/modules/api.js" as="script">
    <link rel="preload" href="fonts/main-font.woff2" as="font" type="font/woff2" crossorigin>
</head>
<body>
    <div id="app">
        <div class="loading">
            <h1>应用加载中...</h1>
            <p>首次加载可能需要一些时间</p>
        </div>
    </div>
    
    <script>
        // 内联的关键脚本,立即执行
        (function() {
            // 显示加载进度
            const loadingEl = document.querySelector('.loading');
            const progressEl = document.createElement('div');
            progressEl.innerHTML = '<progress id="loadProgress" value="0" max="100">0%</progress>';
            loadingEl.appendChild(progressEl);
            
            // 监听脚本加载进度
            const scripts = document.querySelectorAll('script[src]');
            let loaded = 0;
            
            scripts.forEach(script => {
                if (script.hasAttribute('defer') || script.hasAttribute('async')) {
                    script.addEventListener('load', () => {
                        loaded++;
                        const progress = (loaded / scripts.length) * 100;
                        document.getElementById('loadProgress').value = progress;
                    });
                }
            });
        })();
    </script>
</body>
</html>

对应的模块化代码结构:

// js/core/framework.js - 框架核心
window.MySPA = {
    // 核心功能
    createApp: function(config) {
        return {
            config: config,
            mount: function(selector) {
                console.log('挂载应用到:', selector);
            }
        };
    },
    
    // 工具函数
    utils: {
        fetch: function(url) {
            return fetch(url).then(r => r.json());
        }
    }
};

// js/modules/api.js - API 模块
MySPA.modules = MySPA.modules || {};

MySPA.modules.api = {
    baseURL: '/api',
    
    request: function(endpoint, options) {
        return fetch(this.baseURL + endpoint, options)
            .then(response => {
                if (!response.ok) throw new Error('API Error');
                return response.json();
            });
    },
    
    get: function(endpoint) {
        return this.request(endpoint, {method: 'GET'});
    },
    
    post: function(endpoint, data) {
        return this.request(endpoint, {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify(data)
        });
    }
};

// js/app.js - 主应用
document.addEventListener('DOMContentLoaded', function() {
    // 创建应用实例
    const app = MySPA.createApp({
        title: '我的SPA应用',
        version: '1.0.0'
    });
    
    // 初始化路由
    if (MySPA.modules.router) {
        MySPA.modules.router.init();
    }
    
    // 挂载应用
    app.mount('#app');
    
    // 隐藏加载界面
    const loadingEl = document.querySelector('.loading');
    if (loadingEl) {
        loadingEl.style.opacity = '0';
        setTimeout(() => loadingEl.remove(), 300);
    }
});
多脚本协同工作时的加载策略组合技巧

当页面需要加载多个相互依赖的脚本时,需要更精细的策略:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>复杂依赖管理</title>
    
    <!-- 策略1:分层加载 -->
    <!-- 核心层:框架和工具库 -->
    <script defer src="libs/core.js"></script>
    <script defer src="libs/utils.js"></script>
    
    <!-- 服务层:API 和数据处理 -->
    <script defer src="services/api.js"></script>
    <script defer src="services/cache.js"></script>
    
    <!-- 表现层:UI 组件 -->
    <script defer src="components/base.js"></script>
    <script defer src="components/form.js"></script>
    <script defer src="components/table.js"></script>
    
    <!-- 应用层:业务逻辑 -->
    <script defer src="app/main.js"></script>
    
    <!-- 策略2:条件加载 -->
    <script>
        // 根据浏览器特性决定加载策略
        const isModernBrowser = 'IntersectionObserver' in window;
        
        if (isModernBrowser) {
            // 现代浏览器用 defer 加载完整功能
            document.write('<script defer src="app/modern.js"><\/script>');
        } else {
            // 旧浏览器用 async 加载兼容版本
            document.write('<script async src="app/legacy.js"><\/script>');
        }
        
        // 根据页面类型加载不同模块
        const pageType = document.body.dataset.pageType;
        
        switch(pageType) {
            case 'dashboard':
                import('./modules/dashboard.js');
                break;
            case 'profile':
                import('./modules/profile.js');
                break;
            default:
                import('./modules/common.js');
        }
    </script>
    
    <!-- 策略3:动态加载 -->
    <script>
        // 动态脚本加载器
        class ScriptLoader {
            constructor() {
                this.cache = new Map();
            }
            
            load(src, options = {}) {
                // 如果已经加载过,直接返回缓存
                if (this.cache.has(src)) {
                    return this.cache.get(src);
                }
                
                // 创建加载 Promise
                const loadPromise = new Promise((resolve, reject) => {
                    const script = document.createElement('script');
                    script.src = src;
                    
                    // 根据选项设置属性
                    if (options.async) script.async = true;
                    if (options.defer) script.defer = true;
                    if (options.module) script.type = 'module';
                    
                    script.onload = resolve;
                    script.onerror = reject;
                    
                    document.head.appendChild(script);
                });
                
                this.cache.set(src, loadPromise);
                return loadPromise;
            }
            
            // 批量加载
            loadAll(scripts) {
                return Promise.all(
                    scripts.map(({src, ...options}) => this.load(src, options))
                );
            }
            
            // 按顺序加载
            loadSequence(scripts) {
                return scripts.reduce((promise, {src, ...options}) => {
                    return promise.then(() => this.load(src, options));
                }, Promise.resolve());
            }
        }
        
        // 使用示例
        const loader = new ScriptLoader();
        
        // 并行加载非关键模块
        loader.loadAll([
            {src: 'widgets/chat.js', async: true},
            {src: 'widgets/social.js', async: true},
            {src: 'widgets/analytics.js', async: true}
        ]);
        
        // 按顺序加载关键模块
        loader.loadSequence([
            {src: 'core/config.js', defer: true},
            {src: 'core/auth.js', defer: true},
            {src: 'app/main.js', defer: true}
        ]);
    </script>
</head>
<body>
    <h1>复杂依赖管理示例</h1>
</body>
</html>

踩坑实录:那些年我们被 async/defer 坑过的地方

脚本顺序错乱导致功能失效?可能是你忽略了依赖

真实案例:某电商网站的购物车功能突然失效,排查发现是因为新加入的促销脚本使用了async,导致它可能在购物车脚本之前执行:

<!-- 错误的加载方式 -->
<script async src="promotion.js"></script>  <!-- 新加的促销脚本 -->
<script defer src="shopping-cart.js"></script>  <!-- 原有的购物车脚本 -->

<!-- 
promotion.js 的内容:
-->
<script async>
// 错误的假设:以为购物车一定已经初始化
if (window.ShoppingCart) {
    ShoppingCart.applyPromotion('SUMMER2024');
} else {
    // 这里会执行,因为 ShoppingCart 可能还没加载
    console.error('购物车模块未就绪!');
}
</script>

<!-- 
shopping-cart.js 的内容:
-->
<script defer>
window.ShoppingCart = {
    items: [],
    applyPromotion: function(code) {
        console.log('应用促销码:', code);
        // 实际逻辑...
    }
};
</script>

解决方案1:使用事件系统解耦

<script async>
// promotion.js - 使用事件系统
document.addEventListener('shoppingCartReady', function() {
    ShoppingCart.applyPromotion('SUMMER2024');
});

// 或者轮询检查(不推荐,仅作示例)
const checkCart = setInterval(function() {
    if (window.ShoppingCart) {
        clearInterval(checkCart);
        ShoppingCart.applyPromotion('SUMMER2024');
    }
}, 100);
</script>

解决方案2:统一使用defer并按顺序排列

<!-- 正确的加载方式 -->
<script defer src="shopping-cart.js"></script>
<script defer src="promotion.js"></script>

<!-- 
promotion.js 现在可以安全地假设 ShoppingCart 已存在
-->
<script defer>
// 这里可以放心使用 ShoppingCart
ShoppingCart.applyPromotion('SUMMER2024');
</script>
误用 async 导致 DOM 操作报错的典型场景
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>DOM 操作踩坑示例</title>
    
    <!-- 错误示例:async 脚本尝试操作可能不存在的 DOM -->
    <script async>
        // 这个脚本可能在 DOM 还没解析完就执行
        const header = document.getElementById('header');
        header.style.backgroundColor = 'red';  // 可能报错:Cannot read property 'style' of null
    </script>
</head>
<body>
    <h1 id="header">页面标题</h1>
    
    <script>
        // 记录错误
        window.addEventListener('error', function(e) {
            console.error('捕获到错误:', e.message);
        });
    </script>
</body>
</html>

正确的处理方式:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>DOM 操作正确示例</title>
    
    <!-- 方案1:使用 defer 代替 async -->
    <script defer>
        const header = document.getElementById('header');
        if (header) {
            header.style.backgroundColor = 'red';
        }
    </script>
    
    <!-- 方案2:使用 DOMContentLoaded 事件 -->
    <script async>
        document.addEventListener('DOMContentLoaded', function() {
            const header = document.getElementById('header');
            if (header) {
                header.style.backgroundColor = 'blue';
            }
        });
    </script>
    
    <!-- 方案3:使用 MutationObserver 作为终极方案 -->
    <script async>
        // 等待特定元素出现
        function waitForElement(selector, callback) {
            const element = document.querySelector(selector);
            if (element) {
                callback(element);
                return;
            }
            
            const observer = new MutationObserver(function(mutations, obs) {
                const element = document.querySelector(selector);
                if (element) {
                    callback(element);
                    obs.disconnect();
                }
            });
            
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }
        
        // 使用示例
        waitForElement('#header', function(header) {
            header.style.backgroundColor = 'green';
        });
    </script>
</head>
<body>
    <h1 id="header">页面标题</h1>
</body>
</html>
defer 脚本迟迟不执行?检查是不是放错了位置
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>defer 位置问题</title>
    
    <!-- 问题:defer 脚本放在 iframe 里 -->
    <script defer>
        console.log('这个 defer 脚本可能不会按预期执行');
    </script>
</head>
<body>
    <!-- 如果页面内容是通过 document.write 动态生成的,defer 可能失效 -->
    <script>
        document.write('<h1>动态生成的内容</h1>');
    </script>
    
    <!-- 解决方案:确保 defer 脚本是静态的 -->
    <!-- 错误示例 -->
    <script>
        // 动态创建的 script 标签,defer 可能无效
        const script = document.createElement('script');
        script.defer = true;
        script.textContent = 'console.log("动态 defer 脚本")';
        document.head.appendChild(script);
    </script>
    
    <!-- 正确示例 -->
    <script defer>
        // 静态定义的 defer 脚本
        console.log('这个一定会按正确时机执行');
    </script>
</body>
</html>

defer 失效的常见原因和解决方案:

// 解决方案1:使用动态导入
async function loadModuleDynamically() {
    try {
        const module = await import('./modules/feature.js');
        module.init();
    } catch (error) {
        console.error('模块加载失败:', error);
    }
}

// 解决方案2:使用加载队列
class ScriptQueue {
    constructor() {
        this.queue = [];
        this.loaded = new Set();
    }
    
    add(src, options = {}) {
        this.queue.push({src, ...options});
        return this;
    }
    
    async execute() {
        for (const {src, async = false} of this.queue) {
            await this.loadScript(src, async);
        }
    }
    
    loadScript(src, async) {
        return new Promise((resolve, reject) => {
            if (this.loaded.has(src)) {
                resolve();
                return;
            }
            
            const script = document.createElement('script');
            script.src = src;
            if (async) script.async = true;
            
            script.onload = () => {
                this.loaded.add(src);
                resolve();
            };
            
            script.onerror = reject;
            document.head.appendChild(script);
        });
    }
}

// 使用示例
const queue = new ScriptQueue();
queue
    .add('js/core.js')
    .add('js/features.js')
    .add('js/app.js')
    .execute()
    .then(() => {
        console.log('所有脚本加载完成');
    });

高手私藏的调试与优化技巧

用 Performance 面板看清脚本加载全过程
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>性能分析演示</title>
    
    <!-- 性能标记 -->
    <script>
        // 创建性能标记
        performance.mark('script-loading-start');
        
        // 监听各个阶段
        const observer = new PerformanceObserver((list) => {
            for (const entry of list.getEntries()) {
                if (entry.entryType === 'resource' && entry.initiatorType === 'script') {
                    console.log('脚本加载性能:', {
                        名称: entry.name.split('/').pop(),
                        加载时间: entry.duration.toFixed(2) + 'ms',
                        开始时间: entry.startTime.toFixed(2) + 'ms',
                        大小: (entry.transferSize / 1024).toFixed(2) + 'KB'
                    });
                }
            }
        });
        
        observer.observe({entryTypes: ['resource']});
    </script>
    
    <!-- 各种脚本加载方式对比 -->
    <script defer src="https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js" 
            onload="performance.mark('lodash-loaded')"></script>
    
    <script async src="https://cdn.jsdelivr.net/npm/moment@2/moment.min.js" 
            onload="performance.mark('moment-loaded')"></script>
    
    <script defer>
        // 所有脚本加载完成后分析
        window.addEventListener('load', function() {
            performance.mark('page-loaded');
            
            // 测量各个阶段
            performance.measure('总加载时间', 'script-loading-start', 'page-loaded');
            performance.measure('Lodash加载时间', 'script-loading-start', 'lodash-loaded');
            performance.measure('Moment加载时间', 'script-loading-start', 'moment-loaded');
            
            // 输出结果
            const measures = performance.getEntriesByType('measure');
            console.table(measures.map(m => ({
                名称: m.name,
                持续时间: m.duration.toFixed(2) + 'ms'
            })));
            
            // 清理
            performance.clearMarks();
            performance.clearMeasures();
        });
    </script>
</head>
<body>
    <h1>性能分析演示</h1>
    <p>打开 Chrome DevTools 的 Performance 面板查看详细数据</p>
    
    <button onclick="analyzePerformance()">手动分析性能</button>
    
    <script>
        function analyzePerformance() {
            // 获取所有脚本资源
            const scripts = performance.getEntriesByType('resource')
                .filter(r => r.initiatorType === 'script');
            
            // 分析加载策略效果
            const analysis = scripts.map(script => {
                const url = new URL(script.name);
                return {
                    脚本: url.pathname.split('/').pop(),
                    策略: script.name.includes('async') ? 'async' : 
                          script.name.includes('defer') ? 'defer' : 'normal',
                    加载时间: script.duration.toFixed(2) + 'ms',
                    阻塞时间: (script.responseEnd - script.requestStart).toFixed(2) + 'ms'
                };
            });
            
            console.table(analysis);
            
            // 提供优化建议
            const slowScripts = scripts.filter(s => s.duration > 1000);
            if (slowScripts.length > 0) {
                console.warn('发现慢加载脚本:', slowScripts);
                console.log('优化建议:');
                console.log('1. 考虑使用 CDN 加速');
                console.log('2. 检查是否可以拆分大脚本');
                console.log('3. 考虑使用代码分割');
            }
        }
    </script>
</body>
</html>
结合 preload 提前拉取关键脚本资源
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>预加载优化演示</title>
    
    <!-- 预加载关键脚本 -->
    <link rel="preload" href="js/critical-feature.js" as="script">
    <link rel="preload" href="js/app-core.js" as="script">
    
    <!-- 预加载字体资源 -->
    <link rel="preload" href="fonts/main.woff2" as="font" type="font/woff2" crossorigin>
    
    <!-- 预连接第三方域名 -->
    <link rel="preconnect" href="https://cdn.example.com">
    <link rel="dns-prefetch" href="https://api.example.com">
    
    <script>
        // 智能预加载器
        class ResourcePreloader {
            constructor() {
                this.preloaded = new Set();
                this.priorityQueue = [];
            }
            
            // 预加载脚本
            preloadScript(src, options = {}) {
                if (this.preloaded.has(src)) {
                    return Promise.resolve();
                }
                
                return new Promise((resolve, reject) => {
                    const link = document.createElement('link');
                    link.rel = 'preload';
                    link.as = 'script';
                    link.href = src;
                    
                    link.onload = () => {
                        this.preloaded.add(src);
                        resolve();
                        
                        // 如果是高优先级,立即执行
                        if (options.immediate) {
                            this.executeScript(src);
                        }
                    };
                    
                    link.onerror = reject;
                    document.head.appendChild(link);
                });
            }
            
            // 执行预加载的脚本
            executeScript(src) {
                const script = document.createElement('script');
                script.src = src;
                script.async = false; // 保持顺序
                document.head.appendChild(script);
            }
            
            // 批量预加载
            async preloadBatch(scripts) {
                // 按优先级排序
                const sorted = scripts.sort((a, b) => (b.priority || 0) - (a.priority || 0));
                
                // 并行预加载
                const promises = sorted.map(script => 
                    this.preloadScript(script.src, script.options)
                );
                
                await Promise.all(promises);
            }
        }
        
        // 使用示例
        const preloader = new ResourcePreloader();
        
        // 预加载关键资源
        preloader.preloadBatch([
            {src: 'js/framework.js', priority: 10, options: {immediate: true}},
            {src: 'js/router.js', priority: 9},
            {src: 'js/components.js', priority: 8},
            {src: 'js/analytics.js', priority: 3}
        ]).then(() => {
            console.log('关键资源预加载完成');
        });
    </script>
</head>
<body>
    <h1>预加载优化演示</h1>
    
    <button onclick="testPreload()">测试预加载效果</button>
    
    <script>
        function testPreload() {
            const perf = performance.getEntriesByType('resource');
            const preloaded = perf.filter(r => r.initiatorType === 'link');
            
            console.table(preloaded.map(r => ({
                资源: r.name.split('/').pop(),
                类型: r.initiatorType,
                预加载时间: r.duration.toFixed(2) + 'ms'
            })));
        }
    </script>
</body>
</html>
动态插入 script 时如何手动控制 async/defer 行为
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>动态脚本控制</title>
    
    <script>
        // 高级脚本加载器
        class AdvancedScriptLoader {
            constructor(options = {}) {
                this.options = {
                    defaultAsync: false,
                    defaultDefer: false,
                    timeout: 10000,
                    retryCount: 3,
                    ...options
                };
                
                this.loading = new Map();
                this.cache = new Map();
            }
            
            // 创建脚本元素
            createScript(src, options = {}) {
                const script = document.createElement('script');
                script.src = src;
                script.type = options.type || 'text/javascript';
                
                // 设置 async 和 defer
                if (options.async !== undefined) {
                    script.async = options.async;
                } else if (this.options.defaultAsync) {
                    script.async = true;
                }
                
                if (options.defer !== undefined) {
                    script.defer = options.defer;
                } else if (this.options.defaultDefer) {
                    script.defer = true;
                }
                
                // 添加自定义属性
                if (options.dataset) {
                    Object.entries(options.dataset).forEach(([key, value]) => {
                        script.dataset[key] = value;
                    });
                }
                
                return script;
            }
            
            // 带重试机制的加载
            async loadWithRetry(src, options = {}) {
                const cacheKey = `${src}-${JSON.stringify(options)}`;
                
                if (this.cache.has(cacheKey)) {
                    return this.cache.get(cacheKey);
                }
                
                if (this.loading.has(cacheKey)) {
                    return this.loading.get(cacheKey);
                }
                
                const loadPromise = this.attemptLoad(src, options);
                this.loading.set(cacheKey, loadPromise);
                
                try {
                    const result = await loadPromise;
                    this.cache.set(cacheKey, result);
                    return result;
                } catch (error) {
                    this.loading.delete(cacheKey);
                    throw error;
                }
            }
            
            attemptLoad(src, options, attempt = 1) {
                return new Promise((resolve, reject) => {
                    const script = this.createScript(src, options);
                    const timeout = setTimeout(() => {
                        reject(new Error(`Script load timeout: ${src}`));
                    }, this.options.timeout);
                    
                    script.onload = () => {
                        clearTimeout(timeout);
                        resolve({
                            src,
                            loaded: true,
                            attempt
                        });
                    };
                    
                    script.onerror = () => {
                        clearTimeout(timeout);
                        
                        if (attempt < this.options.retryCount) {
                            console.warn(`Retry loading ${src}, attempt ${attempt + 1}`);
                            this.attemptLoad(src, options, attempt + 1)
                                .then(resolve)
                                .catch(reject);
                        } else {
                            reject(new Error(`Failed to load script after ${attempt} attempts: ${src}`));
                        }
                    };
                    
                    document.head.appendChild(script);
                });
            }
            
            // 并行加载多个脚本
            async loadAll(scripts) {
                const promises = scripts.map(script => {
                    if (typeof script === 'string') {
                        return this.loadWithRetry(script);
                    }
                    return this.loadWithRetry(script.src, script.options);
                });
                
                return Promise.all(promises);
            }
            
            // 按顺序加载脚本
            async loadSequence(scripts) {
                const results = [];
                
                for (const script of scripts) {
                    if (typeof script === 'string') {
                        results.push(await this.loadWithRetry(script));
                    } else {
                        results.push(await this.loadWithRetry(script.src, script.options));
                    }
                }
                
                return results;
            }
            
            // 条件加载
            async loadConditional(condition, src, options) {
                if (typeof condition === 'function' ? condition() : condition) {
                    return this.loadWithRetry(src, options);
                }
                return null;
            }
        }
        
        // 使用示例
        const loader = new AdvancedScriptLoader({
            defaultDefer: true,
            timeout: 5000,
            retryCount: 2
        });
        
        // 示例1:加载不同类型的脚本
        async function loadDifferentScripts() {
            try {
                // 加载关键框架(defer,按顺序)
                await loader.loadSequence([
                    {src: 'js/framework.js', options: {defer: true}},
                    {src: 'js/app.js', options: {defer: true}}
                ]);
                
                // 加载第三方插件(async,并行)
                await loader.loadAll([
                    {src: 'plugins/analytics.js', options: {async: true}},
                    {src: 'plugins/chat.js', options: {async: true}},
                    {src: 'plugins/social.js', options: {async: true}}
                ]);
                
                console.log('所有脚本加载完成');
            } catch (error) {
                console.error('脚本加载失败:', error);
            }
        }
        
        // 示例2:条件加载
        async function loadFeatureFlags() {
            // 只在需要时加载实验性功能
            await loader.loadConditional(
                () => localStorage.getItem('enableBetaFeatures') === 'true',
                'js/beta-features.js',
                {async: true}
            );
            
            // 根据浏览器特性加载 polyfill
            await loader.loadConditional(
                !window.IntersectionObserver,
                'https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver',
                {defer: true}
            );
        }
        
        // 示例3:错误恢复
        async function loadWithFallback() {
            const primaryLoader = loader.loadWithRetry('https://primary-cdn.com/app.js');
            const fallbackLoader = loader.loadWithRetry('https://fallback-cdn.com/app.js');
            
            try {
                // 优先使用主 CDN
                await primaryLoader;
            } catch (error) {
                console.warn('主 CDN 失败,使用备用 CDN');
                // 主 CDN 失败时使用备用
                await fallbackLoader;
            }
        }
    </script>
</head>
<body>
    <h1>动态脚本控制演示</h1>
    
    <button onclick="loadDifferentScripts()">加载不同类型脚本</button>
    <button onclick="loadFeatureFlags()">条件加载功能</button>
    <button onclick="loadWithFallback()">错误恢复测试</button>
</body>
</html>

别让浏览器猜你在想什么——写清楚,跑得快

经过这一番深入浅出的探讨,相信你已经对asyncdefer这对孪生兄弟有了全新的认识。它们就像是你工具箱里的两把瑞士军刀——看起来相似,但各有各的拿手好戏。

记住这些金科玉律:

  1. 独立小脚本爱async:就像快递小哥,包裹一到立马派送,适合那些不依赖别人的独立脚本
  2. 模块大家庭用defer:像排队买奶茶,先来后到井然有序,适合有依赖关系的模块
  3. 关键路径要内联:CSS和关键JS直接塞进HTML,别让浏览器来回跑
  4. 第三方代码要隔离:广告、统计这些"外来户",用async别让它们耽误正事

最后,送你一个万能模板,下次写页面直接照抄就行:

<!DOCTYPE html>
<html>
<head>
    <!-- 关键样式内联 -->
    <style>/* 关键CSS */</style>
    
    <!-- 核心脚本用defer -->
    <script defer src="js/framework.js"></script>
    <script defer src="js/app.js"></script>
    
    <!-- 第三方脚本用async -->
    <script async src="https://third-party.com/analytics.js"></script>
    
    <!-- 预加载关键资源 -->
    <link rel="preload" href="js/critical.js" as="script">
</head>
<body>
    <!-- 页面内容 -->
    
    <!-- 动态功能最后加载 -->
    <script>
        // 页面加载完成后的初始化
        window.addEventListener('load', function() {
            // 非关键功能可以在这里动态加载
            import('./features/nice-to-have.js');
        });
    </script>
</body>
</html>

下次再有人问你"async和defer到底有啥区别",别急着解释,直接把这篇文章甩给他,然后说:"自己看,看完请我喝奶茶!"毕竟,好知识就像好代码——值得分享,更值得用一杯奶茶的代价来交换。

记住,给你的script标签加上合适的属性,不仅是对浏览器负责,更是对用户最大的尊重。因为在这个快节奏的时代,用户的耐心就像IE浏览器的市场份额——越来越少啦!

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
上一篇:HTML5 Canvas实现全屏黑客帝国文字掉落效果 下一篇:很抱歉没有了
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐