JavaScript Defer vs Async Explained (2025-04-09 17:24:22)
Question:使用 defer 或者 async 异步下载,是新开了一个线程吗Answer:不是的,defer 和 async 并不会开启新的线程。JavaScript 是单线程的语言,这两个属性只是改变了脚本的加载和执行时序。让我来详细解释一下:
- defer 属性:
- 脚本的下载是异步的,不会阻塞 HTML 解析
- 脚本会等到 HTML 解析完成后,按照它们在文档中的顺序执行
- 在 DOMContentLoaded 事件之前执行完成
1 | <script defer src="script.js"></script> |
- async 属性:
- 脚本的下载是异步的,不会阻塞 HTML 解析
- 脚本下载完成后立即执行,不保证执行顺序
- 可能在 DOMContentLoaded 事件之前或之后执行
1 | <script async src="script.js"></script> |
1 | HTML 解析 ----||---- HTML 解析 |
1 | HTML 解析 -------------------| |
1 | HTML 解析 ------------| |
Question:浏览器本身可以并发请求,如果在一个页面底部加载 script 时,加不加 defer 有区别吗
Answer:如果脚本标签放在页面底部(之前),加不加 defer 实际上区别很小,因为此时 HTML 已经基本解析完成。不过还是存在一些细微差别:
- 不加 defer 的情况:
1
2
3
4
5<body>
<!-- HTML 内容 -->
<script src="script1.js"></script>
<script src="script2.js"></script>
</body>
- 脚本会按顺序下载和执行
- script1.js 必须下载并执行完成后,才会开始处理 script2.js
- 会阻塞 DOMContentLoaded 事件
- 加上 defer 的情况:
1
2
3
4
5<body>
<!-- HTML 内容 -->
<script defer src="script1.js"></script>
<script defer src="script2.js"></script>
</body>
- 脚本会并行下载
- 仍然按照顺序执行,但是不会互相阻塞下载
- 会在 DOMContentLoaded 事件之前执行
- 并行下载:defer 允许多个脚本并行下载,而普通脚本是串行的
- DOMContentLoaded 时机:defer 脚本会在 DOMContentLoaded 之前执行完成,而普通脚本会阻塞这个事件
- 如果脚本之间有依赖关系,使用 defer 更安全,因为它保证执行顺序
- 如果是独立的脚本,可以考虑使用 async,执行时机更灵活
- 如果只有一个简单的脚本,放在底部不加 defer 也是可以的
Question:既然不加defer 的时候,脚本按顺序下载执行,那么浏览器的并发请求还有什么用呢
Answer:浏览器的并发请求主要用于其他资源的并行下载,而不仅仅是脚本文件。让我解释一下:
- 浏览器并发请求的应用场景:
- 图片资源 (img)
- 样式文件 (css)
- 字体文件 (font)
- 媒体文件 (video/audio)
- XHR/Fetch 请求
- 其他静态资源
1 | <head> |
- 所有图片会并行下载
- 两个 CSS 文件会并行下载
- 但是脚本文件会串行下载和执行
- 为什么脚本要串行?
- JavaScript 可能会修改 DOM
- 脚本之间可能有依赖关系
- 保证执行顺序的可预测性
- 避免竞态条件
- 现代优化方案:
1
2
3
4
5
6
7
8
9
10<!-- 并行下载,按序执行 -->
<script defer src="script1.js"></script>
<script defer src="script2.js"></script>
<!-- 并行下载,谁先下载完谁先执行 -->
<script async src="independent1.js"></script>
<script async src="independent2.js"></script>
<!-- 预加载提示 -->
<link rel="preload" href="script1.js" as="script">
- 优化整体页面加载性能
- 充分利用带宽资源
- 减少用户等待时间
- 提高资源加载效率
Question:确认下js文件下载和执行都是串行的吗
Answer:我需要纠正之前的说法。对于普通的 script 标签(不带 defer/async):
- 下载过程:
- 下载是可以并行的
- 浏览器通常会限制并发连接数(通常是每个域名 6-8 个并发)
- 多个 script 标签的资源可以同时下载
- 执行过程:
- 执行是严格串行的
- 必须按照 script 标签在文档中的顺序执行
- 一个脚本执行完成后才会执行下一个
1 | <script src="big-file.js"></script> <!-- 5MB, 需要10秒下载 --> |
- big-file.js 和 small-file.js 会同时开始下载
- small-file.js 可能先下载完成,但要等 big-file.js 下载并执行完才能执行
- 执行顺序一定是 big-file.js 然后是 small-file.js
- 下载可以并行
- 执行必须串行
- 后面的脚本执行必须等待前面的脚本下载并执行完成
- defer:并行下载,按序执行
- async:并行下载,下载完就执行,不保证顺序
Question:如果是并行下载,串行执行的,那加不加 defer 还有什么区别
Answer:主要区别在于 defer 改变了脚本的执行时机和 HTML 解析的阻塞行为:
- 不带 defer 的普通脚本:
1
2<script src="script1.js"></script>
<script src="script2.js"></script>
- 并行下载脚本
- 遇到 script 标签时会暂停 HTML 解析
- 等待当前脚本下载并执行完成后,才继续解析 HTML
- 执行时机:立即执行(阻塞 HTML 解析)
1 | HTML解析 ----||---- HTML解析 ----||---- HTML解析 |
- 带 defer 的脚本:
1
2<script defer src="script1.js"></script>
<script defer src="script2.js"></script>
- 并行下载脚本
- 不阻塞 HTML 解析,HTML 可以继续解析
- 所有脚本等到 HTML 解析完成后才执行
- 执行时机:HTML 解析完成后,DOMContentLoaded 事件之前
1 | HTML解析 --------------------------| |
- 更好的性能:
- HTML 解析不被阻塞
- 用户可以更快看到页面内容
- 特别适合放在
<head>
中的脚本
- 更可预测的执行环境:
- 确保在 DOM 完全准备好后执行
- 不用担心 DOM 元素是否存在
- 适合依赖 DOM 的脚本
- 脚本放在
<head>
中时 - 脚本依赖 DOM 结构时
- 页面加载性能关键时
Question:多个加了 defer 的 script 执行顺序是什么样
Answer:多个带 defer 的脚本特点是:
- 并行下载
- 按照在 HTML 中出现的顺序执行
- 都在 HTML 解析完成后,DOMContentLoaded 之前执行
1 | <script defer src="script1.js"></script> <!-- 5MB,需要10秒下载 --> |
- script1.js、script2.js、script3.js 同时开始下载
- 虽然 script2.js 可能最先下载完成,但要等 script1.js 执行完才能执行
- script3.js 必须等 script2.js 执行完才能执行
1 | HTML解析 --------------------------------| |
- 保证了脚本的执行顺序,适合有依赖关系的脚本
1
2<script defer src="jquery.js"></script>
<script defer src="jquery-plugin.js"></script> <!-- 依赖 jquery --> - 比较 async 的行为:
1
2<script async src="script1.js"></script>
<script async src="script2.js"></script>
- async 也是并行下载
- 但是谁先下载完谁先执行
- 不保证执行顺序
- 适合独立的脚本(如统计代码、广告代码等)
- 有依赖关系的脚本用 defer
- 完全独立的脚本用 async
- 都不加则会阻塞 HTML 解析
Question:css 可以添加 prefetch 吗
Answer:是的,CSS 文件可以使用 prefetch,而且还有其他几种预加载的方式。我来详细解释一下:
- prefetch(预获取):
1
<link rel="prefetch" href="styles.css" as="style">
- 低优先级加载
- 浏览器空闲时获取
- 为未来可能用到的资源做准备
- 适合下一个页面可能需要的资源
- preload(预加载):
1
<link rel="preload" href="critical.css" as="style">
- 高优先级加载
- 当前页面必需的资源
- 尽早发起请求
- 适合当前页面关键资源
- preconnect(预连接):
1
<link rel="preconnect" href="https://cdn.example.com">
- 预先建立连接
- 包括 DNS 查找、TCP 握手、TLS 协商
- 适合确定要从某个域名加载资源,但不确定具体是什么资源
- dns-prefetch(DNS预解析):
1
<link rel="dns-prefetch" href="https://cdn.example.com">
- 仅预解析 DNS
- 比 preconnect 轻量
- 适合可能会用到的域名
- 当前页面关键 CSS:
1
2<link rel="preload" href="critical.css" as="style">
<link rel="stylesheet" href="critical.css"> - 下一个页面可能需要的 CSS:
1
<link rel="prefetch" href="next-page.css" as="style">
- 从 CDN 加载的 CSS:
1
2<link rel="preconnect" href="https://cdn.example.com">
<link rel="stylesheet" href="https://cdn.example.com/styles.css">
- preload 的资源如果 3s 内没有被使用,浏览器控制台会警告
- prefetch 的资源可能会被浏览器忽略(如移动设备或弱网环境)
- 不要过度预加载,会占用带宽
- preload 和实际的资源引用都要写,不然资源不会被使用
- 当前页面必需的 CSS → preload
- 下一个页面可能需要的 CSS → prefetch
- 跨域资源 → preconnect
- 只需要 DNS 优化 → dns-prefetch
Question:webpack 中配置 externals 的,还会被打包吗
Answer:不会,配置为 externals 的模块会被排除在打包文件之外。这些模块被认为是外部依赖,webpack 不会对其进行打包处理。举个例子:
- webpack 配置:
1
2
3
4
5
6
7
8// webpack.config.js
module.exports = {
externals: {
jquery: 'jQuery', // 全局变量名
react: 'React',
'react-dom': 'ReactDOM'
}
} - 在代码中使用:
1
2
3
4
5
6import $ from 'jquery'; // 不会被打包
import React from 'react'; // 不会被打包
// 使用这些库
$('#app');
React.createElement('div'); - HTML 中需要手动引入:
1
2
3<!-- 需要在 HTML 中手动引入这些外部依赖 -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js"></script>
- 使用 CDN 资源:
- 减小打包体积
- 利用 CDN 缓存
- 并行加载提升性能
- 避免重复打包:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// webpack.config.js
module.exports = {
externals: {
// 对于 commonjs 模块
lodash: 'lodash',
// 对于 UMD 模块
jquery: {
commonjs: 'jquery',
commonjs2: 'jquery',
amd: 'jquery',
root: '$' // 全局变量名
}
}
} - 不同格式的 externals:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20module.exports = {
externals: {
// 简单的全局变量
jquery: 'jQuery',
// 对象形式,指定不同模块系统
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react'
},
// 数组形式
subtract: ['./math', 'subtract'],
// 正则形式
/^(jquery|\$)$/i: 'jquery'
}
}
- externals 配置的模块必须在运行环境中可用
- 需要确保 HTML 中引入的版本与代码兼容
- 注意全局变量名的大小写
- 考虑模块加载顺序(如果模块间有依赖关系)
Question:网络浏览器面板中的优化级:high,highest, low, lowest ,如何在前端控制这些资源的优先级呢
Answer:前端可以通过多种方式控制资源的加载优先级:
- 使用
fetchpriority
属性(最新的显式优先级控制):1
2
3
4
5
6
7
8
9
10<!-- 图片优先级 -->
<img src="hero.jpg" fetchpriority="high" />
<img src="lazy.jpg" fetchpriority="low" />
<!-- 脚本优先级 -->
<script src="critical.js" fetchpriority="high"></script>
<script src="non-critical.js" fetchpriority="low"></script>
<!-- CSS优先级 -->
<link rel="stylesheet" href="critical.css" fetchpriority="high" /> - 使用
preload
/prefetch
影响优先级:1
2
3
4
5
6<!-- 高优先级加载 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero.jpg" as="image">
<!-- 低优先级加载 -->
<link rel="prefetch" href="next-page.css" as="style"> - 通过资源位置影响优先级:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<head>
<!-- head中的CSS默认highest优先级 -->
<link rel="stylesheet" href="critical.css">
<!-- head中的script默认为low -->
<script src="script.js"></script>
<!-- 添加async降低优先级 -->
<script async src="async-script.js"></script>
</head>
<body>
<!-- 视口中的图片默认high优先级 -->
<img src="hero.jpg" />
<!-- 延迟加载的图片low优先级 -->
<img loading="lazy" src="lazy.jpg" />
</body> - 使用 JavaScript 动态控制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 使用 Fetch API 设置优先级
fetch('/api/data', {
priority: 'high' // 'high' | 'low' | 'auto'
});
// 动态创建元素时设置优先级
const img = new Image();
img.fetchPriority = 'high';
img.src = 'hero.jpg';
// 使用 Priority Hints API
const hint = document.createElement('link');
hint.rel = 'preload';
hint.as = 'image';
hint.href = 'hero.jpg';
hint.fetchPriority = 'high';
document.head.appendChild(hint); - 常见资源的默认优先级:
1
2
3
4
5
6
7
8
9
10
11
12
13<!-- Highest -->
<link rel="stylesheet"> <!-- 在 head 中的 CSS -->
<!-- High -->
<img src="hero.jpg"> <!-- 视口内的图片 -->
<script src="sync.js"> <!-- body 底部的同步脚本 -->
<!-- Low -->
<img loading="lazy" src="lazy.jpg"> <!-- 懒加载图片 -->
<script async src="async.js"> <!-- 异步脚本 -->
<!-- Lowest -->
<link rel="prefetch"> <!-- 预获取资源 -->
- 关键资源优先级提升:
1
2
3<!-- 首屏关键资源 -->
<link rel="stylesheet" href="critical.css" fetchpriority="high">
<img src="hero.jpg" fetchpriority="high"> - 非关键资源优先级降低:
1
2
3<!-- 非关键资源 -->
<img loading="lazy" fetchpriority="low" src="below-fold.jpg">
<script defer fetchpriority="low" src="non-critical.js"></script> - 组合使用不同策略:
1
2
3
4
5
6
7
8
9<!-- 预连接 + 高优先级 -->
<link rel="preconnect" href="https://cdn.example.com">
<link rel="stylesheet"
href="https://cdn.example.com/style.css"
fetchpriority="high">
<!-- 预加载 + 异步加载 -->
<link rel="preload" href="font.woff2" as="font" crossorigin>
<script async src="analytics.js" fetchpriority="low"></script>
- fetchpriority 是较新的属性,需要注意浏览器兼容性
- 不要过度使用高优先级,会影响整体性能
- 优先级设置要结合实际场景和用户体验
- 可以通过 Chrome DevTools 的 Network 面板观察优先级效果
Question:rel=preload 添加到 css 资源上的作用
Answer:rel="preload" 添加到 CSS 资源上主要用于提前声明重要的 CSS 资源,让浏览器更早地发现并下载这些资源。主要作用和使用场景如下:
- 基本用法:
1
2
3
4<!-- 预加载关键 CSS -->
<link rel="preload" href="critical.css" as="style">
<!-- 实际使用该 CSS -->
<link rel="stylesheet" href="critical.css"> - 主要作用:
- 提高优先级(设置为 Highest)
- 提前发现资源
- 并行下载
- 不阻塞 HTML 解析
- 常见使用场景:
1 | <head> |
1 | <link |
1 | <!-- 响应式设计中的条件预加载 --> |
- 注意事项:
1 | <!-- 错误:预加载但未使用 --> |
1 | <!-- 不好的实践:重复的 URL --> |
1 | <!-- 完整的预加载声明 --> |
- 性能优化建议:
1 | <!-- DNS预解析 --> |
1 | <!-- 最高优先级加载 --> |
- 只预加载当前页面必需的关键 CSS
- 确保预加载的资源会被使用
- 注意浏览器的缓存策略
- 监控预加载对性能的实际影响
- 不要过度预加载,可能会影响其他资源加载
Question:会阻塞页面吗
Answer:rel="preload" 本身不会阻塞页面渲染,但需要区分几种情况:
- 单纯的 preload:
1
<link rel="preload" href="style.css" as="style">
- 不会阻塞 HTML 解析
- 不会阻塞页面渲染
- 只是提前下载资源
- preload + stylesheet:
1
2<link rel="preload" href="style.css" as="style">
<link rel="stylesheet" href="style.css">
- preload 不阻塞
- 但 stylesheet 仍然会阻塞渲染
- CSS 仍然是渲染阻塞资源
- 使用 onload 延迟应用 CSS:
1
2
3
4
5<!-- 不阻塞的方式 -->
<link rel="preload" href="style.css" as="style" onload="this.rel='stylesheet'">
<noscript>
<link rel="stylesheet" href="style.css">
</noscript>
- preload 不阻塞
- CSS 在加载完成后才应用
- 可能造成闪烁(FOUC)
- 关键 CSS 内联 + 非关键 CSS 预加载:
1
2
3
4
5
6
7<style>
/* 关键 CSS 直接内联 */
.critical { color: red; }
</style>
<!-- 非关键 CSS 预加载 -->
<link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'">
- 关键样式立即生效
- 非关键样式异步加载
- 最佳性能实践
- 区分关键和非关键资源:
1
2
3
4
5
6
7
8
9
10<!-- 关键 CSS:正常加载 -->
<link rel="stylesheet" href="critical.css">
<!-- 非关键 CSS:预加载 + 异步应用 -->
<link
rel="preload"
href="non-critical.css"
as="style"
onload="this.rel='stylesheet'"
> - 使用媒体查询优化加载:
1
2
3
4
5
6
7
8<!-- 桌面样式延迟加载 -->
<link
rel="preload"
href="desktop.css"
as="style"
media="(min-width: 768px)"
onload="this.rel='stylesheet'"
> - 配合 loadCSS 工具使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<script>
// loadCSS polyfill
function loadCSS(href) {
const preload = document.createElement('link');
preload.rel = 'preload';
preload.as = 'style';
preload.href = href;
preload.onload = function() {
this.rel = 'stylesheet';
};
document.head.appendChild(preload);
}
// 使用
loadCSS('style.css');
</script>
- preload 本身不阻塞
- 但最终 CSS 应用时仍可能阻塞
- 需要根据场景选择合适的加载策略
- 关键 CSS 建议直接加载
- 非关键 CSS 可以预加载+异步应用
Question:cacheGroups 中的 iconfont.js 设置为 lowest
Answer:在 webpack 配置中,我们可以通过 optimization.splitChunks.cacheGroups 来设置 iconfont.js 的加载优先级。这里有几种方法:
- webpack 配置方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
iconfont: {
test: /iconfont\.js$/,
name: 'iconfont',
chunks: 'all',
priority: -20, // 设置较低优先级
enforce: true
}
}
}
}
} - 在 HTML 中使用 fetchpriority:
1
2<!-- webpack 打包后,在 HTML 模板中设置 -->
<script src="/iconfont.chunk.js" fetchpriority="low"></script> - 结合 HTML Webpack Plugin:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: 'index.html',
scriptLoading: 'defer', // 使用 defer
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true
}
})
],
optimization: {
splitChunks: {
cacheGroups: {
iconfont: {
test: /iconfont\.js$/,
name: 'iconfont',
chunks: 'all',
priority: -20,
enforce: true,
attributes: {
fetchpriority: 'low' // 设置加载优先级
}
}
}
}
}
} - 完整的优化配置示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
// 第三方库
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true
},
// iconfont 单独打包
iconfont: {
test: /iconfont\.js$/,
name: 'iconfont',
chunks: 'all',
priority: -20,
enforce: true,
reuseExistingChunk: true
},
// 默认配置
default: {
minChunks: 2,
priority: -30,
reuseExistingChunk: true
}
}
}
}
} - 结合动态导入使用:
1
2
3
4
5
6
7
8
9
10
11
12
13// 在代码中动态导入
const loadIconfont = () => {
return import(
/* webpackChunkName: "iconfont" */
/* webpackPreload: false */
'./iconfont.js'
);
};
// 在适当时机加载
window.addEventListener('load', () => {
loadIconfont();
});
- priority 值越小,优先级越低
- 确保 iconfont.js 的正则匹配准确
- 考虑与其他缓存组的优先级关系
- 可以结合 preload/prefetch 策略
- 注意浏览器兼容性
- 如果 iconfont 不是首屏必需,可以设置为低优先级
- 考虑使用 CSS 的 font-display 属性优化字体加载
- 监控加载性能,确保优化效果
- 结合实际项目需求调整配置
Question:chunks: 'all' 的意思是什么
Answer:chunks: 'all' 表示对所有类型的 chunks 都进行分割处理,包括:
- 三种可选值:
1
2
3
4
5
6
7
8
9cacheGroups: {
vendors: {
chunks: 'initial' // 只处理入口 chunk
// 或
chunks: 'async' // 只处理异步 chunk(按需加载的代码)
// 或
chunks: 'all' // 处理所有 chunk
}
} - 具体含义:
initial
: 只处理入口文件引入的模块1
2// 入口文件
import jQuery from 'jquery'; // 会被处理async
: 只处理动态导入的模块1
2// 动态导入
import('./module.js').then(...); // 会被处理all
: 处理所有模块1
2
3
4
5// 入口文件静态导入
import jQuery from 'jquery'; // 会被处理
// 动态导入
import('./module.js').then(...); // 也会被处理
- 实际配置示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
// 处理所有类型的依赖
vendors: {
test: /[\\/]node_modules[\\/]/,
chunks: 'all',
priority: -10
},
// 只处理异步加载的组件
asyncComponents: {
test: /[\\/]components[\\/]/,
chunks: 'async',
priority: -20
},
// 只处理入口依赖的样式
styles: {
test: /\.css$/,
chunks: 'initial',
priority: -30
}
}
}
}
} - 使用建议:
- 通常建议使用
chunks: 'all'
以获得最佳的代码分割效果 - 特定场景可能需要其他选项:
- 只想分割异步加载的代码时使用
async
- 只想分割入口依赖时使用
initial
- 只想分割异步加载的代码时使用
- 配合其他选项使用会更有效:
- minSize(最小尺寸)
- minChunks(最小引用次数)
- maxAsyncRequests(最大异步请求数)
- maxInitialRequests(最大初始请求数)
Question:vue 中页面开始加载是在 js 哪个事件之后进行的
Answer:在 Vue 中,页面加载涉及几个关键的生命周期事件,主要顺序如下:
- 创建阶段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20new Vue({
beforeCreate() {
// 1. 实例初始化之后,数据观测和事件配置之前
// 此时 data、methods 等还不可用
},
created() {
// 2. 实例创建完成后
// 可以访问 data、computed、methods 等
// 但还没有挂载 DOM,$el 不可用
},
beforeMount() {
// 3. 挂载开始之前被调用
// 相关的 render 函数首次被调用
},
mounted() {
// 4. 挂载完成,DOM 已经渲染
// 可以访问 DOM 元素
// 这里开始真正的页面加载
}
}) - 实际使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23export default {
created() {
// 适合做数据初始化
this.fetchInitialData();
},
mounted() {
// DOM 已经可用,开始加载页面内容
this.initPage();
this.loadComponents();
},
methods: {
fetchInitialData() {
// API 调用等
},
initPage() {
// DOM 操作
// 第三方库初始化
}
}
} - 异步组件加载:
1
2
3
4
5
6
7
8
9
10export default {
components: {
// 异步组件
AsyncComponent: () => import('./AsyncComponent.vue')
},
mounted() {
// 主页面已加载,但异步组件可能还在加载中
}
} - 路由组件加载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// router.js
import Vue from 'vue';
import Router from 'vue-router';
export default new Router({
routes: [
{
path: '/',
component: () => import('./views/Home.vue'),
beforeEnter: (to, from, next) => {
// 路由级别的加载控制
next();
}
}
]
}); - 完整的加载流程控制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37export default {
data() {
return {
isLoading: true,
loadError: null
}
},
async created() {
try {
// 数据预加载
await this.fetchInitialData();
} catch (error) {
this.loadError = error;
}
},
async mounted() {
try {
// DOM 相关初始化
await this.initComponents();
this.isLoading = false;
} catch (error) {
this.loadError = error;
}
},
methods: {
async fetchInitialData() {
// API 调用
},
async initComponents() {
// 组件初始化
}
}
} - 性能优化建议:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21export default {
created() {
// 关键数据优先加载
this.loadCriticalData();
// 非关键数据延迟加载
this.$nextTick(() => {
this.loadNonCriticalData();
});
},
mounted() {
// DOM 操作放在 mounted 中
this.initUI();
// 延迟加载非关键组件
setTimeout(() => {
this.loadDeferredComponents();
}, 0);
}
}
- 页面实际加载从 mounted 开始
- created 适合做数据准备
- mounted 适合做 DOM 操作
- 可以利用异步和延迟加载优化性能
- 考虑使用加载状态管理用户体验
Question:是在页面加载完成之后,即 window.onload 结束之后吗
Answer:不是的,Vue 的生命周期和 window.onload 是独立的事件。Vue 的 mounted 钩子通常会在 window.onload 之前触发。让我解释一下它们的关系:
- 执行顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Vue 实例
new Vue({
mounted() {
console.log('Vue mounted'); // 1. 先执行
}
});
// window 事件
window.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded'); // 2. 然后是这个
});
window.addEventListener('load', () => {
console.log('Window loaded'); // 3. 最后是这个
}); - 区别:
1
2
3
4
5
6
7
8
9
10
11
12
13export default {
mounted() {
// Vue mounted: DOM 已渲染,但外部资源可能还没加载完
console.log('DOM 已渲染');
console.log('图片可能还在加载中');
}
}
window.onload = () => {
// window.onload: 所有资源都加载完成
console.log('所有资源加载完成');
console.log('包括图片、样式、脚本等');
}; - 实际应用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23export default {
mounted() {
// Vue 已挂载,可以操作 DOM
this.initUI();
// 但如果需要等待所有资源
window.addEventListener('load', () => {
this.initAfterFullLoad();
});
},
methods: {
initUI() {
// 可以进行 DOM 操作
// 但外部资源可能还没准备好
},
initAfterFullLoad() {
// 所有资源都已加载完成
// 可以安全地使用依赖外部资源的功能
}
}
} - 最佳实践:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46export default {
data() {
return {
isFullyLoaded: false
}
},
mounted() {
// 立即需要的初始化
this.initCriticalFeatures();
// 监听完整加载
window.addEventListener('load', () => {
this.isFullyLoaded = true;
this.initNonCriticalFeatures();
});
// 监听图片加载
const images = document.querySelectorAll('img');
images.forEach(img => {
if (img.complete) {
this.handleImageLoad(img);
} else {
img.addEventListener('load', () => {
this.handleImageLoad(img);
});
}
});
},
methods: {
initCriticalFeatures() {
// 关键功能初始化
// 不依赖外部资源的操作
},
initNonCriticalFeatures() {
// 非关键功能初始化
// 依赖完整加载的操作
},
handleImageLoad(img) {
// 处理单个图片加载完成
}
}
} - 性能优化建议:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30export default {
mounted() {
// 分阶段初始化
this.initPhase1();
// 使用 requestAnimationFrame 延迟非关键初始化
requestAnimationFrame(() => {
this.initPhase2();
});
// 使用 load 事件处理最终初始化
window.addEventListener('load', () => {
this.initPhase3();
});
},
methods: {
initPhase1() {
// 立即需要的功能
},
initPhase2() {
// 可以稍微延迟的功能
},
initPhase3() {
// 所有资源加载后的功能
}
}
}
- Vue mounted 在 window.onload 之前触发
- mounted 时 DOM 已渲染,但外部资源可能未加载完
- window.onload 表示所有资源都加载完成
- 根据需求选择合适的初始化时机
- 可以组合使用不同事件实现最佳性能
Question:是在 DCL 之后 vue 开始渲染吗
Answer:是的,Vue 的渲染是在 DOMContentLoaded (DCL) 事件之后开始的。让我详细解释一下这个过程:
- 典型的加载顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22document.addEventListener('DOMContentLoaded', () => {
console.log('1. DCL'); // 首先触发
});
new Vue({
beforeCreate() {
console.log('2. Vue beforeCreate');
},
created() {
console.log('3. Vue created');
},
beforeMount() {
console.log('4. Vue beforeMount');
},
mounted() {
console.log('5. Vue mounted');
}
}).$mount('#app');
window.addEventListener('load', () => {
console.log('6. Window load'); // 最后触发
}); - 实际执行流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<!-- index.html -->
<html>
<head>
<script src="vue.js"></script>
</head>
<body>
<div id="app">
<!-- Vue 模板 -->
</div>
<script>
// 1. 解析 HTML
// 2. 触发 DOMContentLoaded
// 3. Vue 开始初始化和渲染
// 4. 外部资源继续加载
// 5. 触发 window.load
</script>
</body>
</html> - 生命周期与事件的关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 监听各个阶段
document.addEventListener('readystatechange', () => {
console.log(document.readyState);
// loading -> interactive(DCL) -> complete(load)
});
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM 准备就绪');
// Vue 将在这之后开始工作
});
new Vue({
mounted() {
console.log('Vue 渲染完成');
// DOM 已经更新
}
}); - 实际应用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 完整的初始化流程
document.addEventListener('DOMContentLoaded', () => {
// DOM 已准备好,可以初始化 Vue
initVueApp();
});
function initVueApp() {
new Vue({
data: {
isReady: false
},
created() {
// 数据初始化
this.initData();
},
mounted() {
// DOM 操作
this.isReady = true;
}
}).$mount('#app');
} - 性能优化建议:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 分阶段加载
document.addEventListener('DOMContentLoaded', () => {
// 1. 核心功能初始化
initCoreFeatures();
// 2. Vue 初始化
const app = new Vue({
// ...配置
});
// 3. 延迟加载非关键功能
requestIdleCallback(() => {
initNonCriticalFeatures();
});
});
// 4. 完整加载后的操作
window.addEventListener('load', () => {
initAfterFullLoad();
}); - 常见的加载优化模式:
1
2
3
4
5
6
7
8
9
10
11
12// 1. 关键资源预加载
<link rel="preload" href="vue.js" as="script">
// 2. 非关键组件异步加载
components: {
'heavy-component': () => import('./HeavyComponent.vue')
}
// 3. 条件渲染等待 DOM 准备
<div v-if="isDomReady">
<!-- 需要 DOM 准备好的内容 -->
</div>
- DCL 事件触发表示 DOM 准备就绪
- Vue 在 DCL 之后开始初始化和渲染
- Vue 的生命周期钩子在 DCL 和 load 之间执行
- 可以利用这个顺序来优化加载性能
- 考虑使用分阶段加载策略
- DCL 只保证 DOM 准备好,不包括外部资源
- Vue 渲染可能在外部资源加载完成之前
- 需要考虑资源依赖关系进行优化
Question:如果body底部有 script 加载,那么DCL 触发是在 script 加载之前还是之后
Answer:对于位于 body 底部的 script,分几种情况:
- 普通 script:
1
2
3
4
5
6
7
8<body>
<div id="app"></div>
<!-- 普通脚本 -->
<script src="large-script.js"></script>
<!-- DCL 会等这个脚本下载和执行完才触发 -->
</body> - async script:
1
2
3
4
5
6
7
8<body>
<div id="app"></div>
<!-- async 脚本不阻塞 DCL -->
<script async src="analytics.js"></script>
<!-- DCL 不会等待 async 脚本 -->
</body> - defer script:
1
2
3
4
5
6
7
8<body>
<div id="app"></div>
<!-- defer 脚本不阻塞 DCL -->
<script defer src="non-critical.js"></script>
<!-- DCL 不会等待 defer 脚本 -->
</body> - 混合情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<body>
<div id="app"></div>
<!-- 会阻塞 DCL -->
<script src="critical.js"></script>
<!-- 不会阻塞 DCL -->
<script defer src="non-critical.js"></script>
<script async src="analytics.js"></script>
<script>
// DCL 触发顺序示意
document.addEventListener('DOMContentLoaded', () => {
console.log('DCL'); // 在 critical.js 之后,其他可能在之前或之后
});
</script>
</body>
- 普通 script:DCL 在脚本加载和执行之后触发
- async script:DCL 不等待这类脚本
- defer script:DCL 不等待这类脚本
- 内联脚本:DCL 等待执行完成
1 | <body> |
Question:@index.html 页面中的这种情况,DCL 是在script加载之前触发吗
Answer:让我分析一下这个 index.html 中的脚本加载情况:
- head 中的脚本(会阻塞 DCL):
1
2
3
4
5
6
7
8<!-- CDN js -->
<script src="<%= htmlWebpackPlugin.options.cdn.js[i].href %>"></script>
<!-- jQuery -->
<script src="//zxxkstatic.zxxk.com/xkwjs/lib/jquery-1.10.2.js?v=0.0128"></script>
<!-- 阿里播放器 -->
<script src="https://g.alicdn.com/de/prismplayer/2.8.2/aliplayer-min.js"></script> - body 底部的脚本(都使用了 async,不会阻塞 DCL):
1
2
3
4<script async src="https://downloadnew.zxxk.com/risk-control-dialog/control-iframe-dialog.min.js"></script>
<script async src="//zxxkstatic.zxxk.com/ued/common/xep/xep.js?v=1.0.0"></script>
<script async src="//zxxkstatic.zxxk.com/xkwjs/xkw.element-monitor.js"></script>
<script async src="//zxxkstatic.zxxk.com/Common/Scripts/searchlog/jquery-search-log.js"></script>
- DCL 会等待 head 中的普通脚本加载和执行完成
- 但不会等待 body 底部的 async 脚本
- DCL 触发顺序:
- head 中的脚本加载完成
- DOM 解析完成
- DCL 触发
- async 脚本在某个时间点加载完成并执行
- DCL 会在 body 底部那些 async 脚本加载之前触发
- 但会等待 head 中的普通脚本加载完成
1 | <!-- head 中非关键脚本也可以使用 defer --> |
Question:这个项目是一个 vue 项目,如果vue组件中存在使用 jquery 的情况,还能加 defer 吗
Answer:
将 jQuery 加上 defer 在 Vue 项目中可能会有问题,特别是当组件直接依赖 jQuery 时。分析这种情况的依赖关系:
- 当前加载顺序:
1
jQuery加载 -> HTML解析 -> DCL触发 -> Vue初始化 -> 组件渲染
- 如果给 jQuery 加 defer:
1
HTML解析 -> DCL触发 -> Vue初始化/jQuery加载(并行) -> 组件渲染
- jQuery 可能在 Vue 组件初始化时还未加载完成
- 组件中的
$
会是undefined
- 导致
$ is not defined
错误
- 不使用 defer,但考虑 preload:
1
2
3<!-- 预加载但不延迟执行 -->
<link rel="preload" href="//zxxkstatic.zxxk.com/xkwjs/lib/jquery-1.10.2.js" as="script">
<script src="//zxxkstatic.zxxk.com/xkwjs/lib/jquery-1.10.2.js"></script> - 使用 Webpack 的 externals 并改变引入方式:
1
2
3
4
5
6
7
8
9
10
11// webpack.config.js
module.exports = {
externals: {
jquery: 'jQuery'
}
}
// 组件中
import $ from 'jquery';
// 或
const $ = window.jQuery; - 条件检查 jQuery 可用性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18export default {
mounted() {
// 确保 jQuery 已加载
const checkJQuery = () => {
if (window.jQuery) {
this.initWithJQuery();
} else {
setTimeout(checkJQuery, 50);
}
};
checkJQuery();
},
methods: {
initWithJQuery() {
// 使用 jQuery 的代码
}
}
}
- 如果组件直接依赖 jQuery,不建议给 jQuery 添加 defer
- 可以通过其他方式优化(如 preload)
- 或者重构代码减少对 jQuery 的依赖
- 最好的做法是在 Vue 项目中减少对 jQuery 的直接使用
Question:可以将外部引用的 css 文件放到底部吗
Answer:
将 CSS 放到底部不是推荐的做法,这可能会导致严重的用户体验问题。原因如下:
- 会导致无样式闪烁(FOUC):
- 页面会先以无样式方式渲染
- 然后突然应用样式
- 造成视觉跳动和闪烁
- 导致布局偏移(CLS):
- 内容先以默认样式布局
- CSS 加载后重新布局
- 这是 Google Core Web Vitals 中的负面指标
- 关键CSS内联 + 非关键CSS异步加载:
1
2
3
4
5
6
7
8
9
10
11
12<head>
<!-- 关键CSS内联 -->
<style>
/* 关键首屏样式 */
body { font-family: sans-serif; margin: 0; }
header { /* 样式... */ }
</style>
<!-- 非关键CSS异步加载 -->
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
</head> - 使用 media 属性延迟非关键样式:
1
2
3
4
5
6
7
8
9
10<head>
<!-- 关键样式立即加载 -->
<link rel="stylesheet" href="critical.css">
<!-- 印刷样式延迟加载 -->
<link rel="stylesheet" href="print.css" media="print">
<!-- 桌面样式延迟加载(手机上) -->
<link rel="stylesheet" href="desktop.css" media="(min-width: 1024px)">
</head> - 使用工具提取关键CSS:
1
2// 使用工具如 Critical, CriticalCSS, Penthouse 等
// 自动提取首屏关键CSS
1 | <script> |
- 关键 CSS 放在
<head>
中 - 使用预加载、延迟加载等技术优化非关键 CSS
- 避免将所有 CSS 放到底部
- 考虑使用现代工具提取和优化 CSS
Question:使用 preload 进行异步加载,异步体现在哪个地方
Answer:
preload
的异步特性主要体现在几个方面:- 并行下载不阻塞渲染:
1
<link rel="preload" href="style.css" as="style" onload="this.rel='stylesheet'">
- 浏览器会在解析到这行代码时立即开始下载资源
- 但不会阻塞 HTML 解析和页面渲染
- 页面可以继续加载,同时 CSS 在后台下载
- 应用与下载分离:
1
2
3
4
5
6
7<!-- 下载过程 -->
<link rel="preload" href="style.css" as="style" onload="this.rel='stylesheet'">
<!-- 或者 -->
<link rel="preload" href="script.js" as="script">
<!-- 应用过程 -->
<script src="script.js" defer></script>
- 资源的下载和应用是分开的两个步骤
- 资源可以在不被使用的情况下提前下载
- 不阻塞 DOMContentLoaded 事件:
1
2
3
4
5
6<link rel="preload" href="large-file.css" as="style" onload="this.rel='stylesheet'">
<script>
document.addEventListener('DOMContentLoaded', () => {
console.log('DCL fired!'); // 不会等待 preload 完成
});
</script>
- 即使大型 CSS 文件还在下载中,DCL 事件也会触发
- 允许基于 DCL 的脚本更早执行
- 手动控制应用时机:
1
2
3
4
5
6
7
8
9
10<link rel="preload" href="style.css" as="style">
<script>
// 通过 JS 控制应用时机
window.addEventListener('load', () => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'style.css'; // 已经被预加载
document.head.appendChild(link);
});
</script>
- 开发者可以精确控制资源的应用时机
- 资源已经在缓存中,应用时不需要再下载
- 示例:异步应用 CSS:
1
2
3
4
5
6
7
8
9<!-- 关键CSS内联 -->
<style>
/* 最基本的样式 */
body { margin: 0; font-family: sans-serif; }
</style>
<!-- 异步加载其他CSS -->
<link rel="preload" href="full-styles.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="full-styles.css"></noscript>
- 页面会首先使用内联的基本样式渲染
- 完整样式异步加载,完成后自动应用
- 用户可以看到基本内容,而不必等待所有样式