引子:写给自己的面试体系笔记,如有概念性错误,烦请指出。
1.加载页面和渲染过程
浏览器从加载页面到渲染页面的过程
加载过程:
-
浏览器网络请求线程的开启
-
浏览器根据 DNS 服务器得到域名的 IP 地址
-
在 TCP IP 通道连接下 向这个 IP 的机器发送 HTTP 请求
-
服务器收到、处理并返回 HTTP 请求
-
浏览器得到返回对应内容
浏览器渲染过程:
- 根据 HTML 结构生成 DOM 树
- 根据 CSS 生成 CSSOM
- 将 DOM 和 CSSOM 整合形成 RenderTree
- 根据 RenderTree 开始布局和绘制
- 遇到
<script>时,会执行并阻塞渲染
2.性能优化
性能模型
- Web Vitals
- 加载性能(LCP) — 显示最大内容元素所需时间
- 交互性(FID) — 首次输入延迟时间
- 视觉稳定性(CLS) — 累积布局配置偏移
此外还有一些:
- First Paint (FP) 页面在屏幕上首次发生视觉变化的时间
- First Contentful Paint(FCP) 浏览器首次绘制来自 DOM 的内容的时间
- Largest Contentful Paint(LCP) 可视区域中最大的内容元素呈现到屏幕上的时间
- DomContentLoaded(DCL) 即
DOMContentLoaded触发时间,DOM 全部解析并渲染完 - Load(L)即
window.onload触发时间,页面内容(包括图片)全部加载完成 - Total Block Time(TBT) 总阻塞时间
- Speed Index 速度指数
- Time to Interactive(TTI) 完全可交互状态
性能测试
- Chrome devtools
- 使用灯塔 Lighthouse测试
- 使用 WebPageTest 测试
注意:建议在 Chrome 隐身模式测试,避免其他缓存的干扰
Performance 可以检测到上述的性能指标,并且有网页快照截图
NetWork 可以看到各个资源的加载时间:
识别是加载慢,还是渲染慢?因为前后端分离
如下图是 github 的性能分析,很明显这是加载慢,渲染很快:
加载慢
- 优化服务端接口
- 使用 CDN
- 压缩文件
- 拆包,异步加载
渲染慢(可参考下面的“首屏优化”)
-
根据业务功能,继续打点监控
-
如果是 SPA 异步加载资源,需要特别关注网络请求的时间
-
Lighthouse 是非常优秀的第三方性能评测工具,支持移动端和 PC 端
-
它支持 Chrome 插件和 npm 安装,国内情况推荐使用后者
# 安装
npm i lighthouse -g
# 检测一个网页,检测完毕之后会打开一个报告网页
lighthouse https://imooc.com/ --view --preset=desktop # 或者 mobile
测试完成之后,lighthouse 给出测试报告:
并且会给出一些优化建议:
首屏优化
- 服务端渲染 SSR
- 服务端直出 html , 浏览器直接下载 html ,解析渲染
- Nuxt.js ( Vue 同构 )
- Next.js ( React 同构 )
- 路由懒加载
- SPA 应用应当注意
- App 预取
- H5 在 App webview 中展示,例如列表浏览文章的时候预取数据(一般是标题、首页文本)
- 分页
- 根据显示设备的高度,设计尽量少的页面内容
- 图片 lazyLoad
- 先加载内容,再加载图片
- 离线包 hybrid
- 提前将 html css js 等下载到 App 内,打开先使用
file://协议加载本地的 html css js,再请求 Ajax 渲染
- 提前将 html css js 等下载到 App 内,打开先使用
- 优化体验:骨架屏 loading
请求和响应优化
-
使用HTTP2(例如服务端推送、多路复用、二进制传输等)
-
通过dns-prefetch减少DNS查询时间
-
静态资源的压缩合并(JS 代码压缩合并、CSS 代码压缩合并、雪碧图)
-
开启 Gzip 压缩
-
减少 Cookie 体积
-
恰当使用本地缓存
-
静态资源缓存(资源名称加hash)
-
使用 CDN 缓存服务器让资源加载更快
-
恰当使用 HTTP 缓存
-
服务端渲染
-
Service Worker 缓存
页面渲染优化
- CSS 放前面,JS 放后面
- 使用 defer(追求加载顺序,适用外部脚本)、async(不追求加载顺序) 延迟加载 JavaScript
- 减少DOM 查询,对 DOM 查询做缓存
- 减少DOM 操作,多个操作尽量合并在一起执行(
DocumentFragment事件:DOM 渲染完即可执行,此时图片、视频还可能没有加载完) - 图片数据的预加载和懒加载
- 恰当使用 Web Worker
- 动画尽量使用 CSS过渡动画 或者 JS的 requestAnimationFrame
- 尽早执行操作(
DOMContentLoaded) - 事件节流防抖
- 使用 SSR 后端渲染,数据直接输出到 HTML 中,减少浏览器使用 JS 模板渲染页面 HTML 的时间
更多的可以看我的这一篇文章:移动端性能优化 - 掘金 (juejin.cn)
preload 和 prefetch
- preload 资源在当前页面使用,会优先加载
- prefetch 资源在未来页面使用,空闲时加载
<head>
<meta charset="utf-8">
<title>JS and CSS preload</title>
<!-- preload -->
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="main.js" as="script">
<!-- prefetch -->
<link rel="prefetch" href="other.js" as="script">
<!-- 引用 css -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>hello</h1>
<!-- 引用 js -->
<script src="main.js" defer></script>
</body>
dns-prefetch 和 preconnect
- 一个 http 请求,第一步就是 DNS 解析得到 IP ,然后进行 TCP 连接。连接成功后再发送请求
- dns-prefetch 即 DNS 预获取,preconnect 即 DNS 预连接
- 当网页请求第三方资源时,可以提前进行 DNS 查询、TCP 连接,以减少请求时的时间
<html>
<head>
<link rel="dns-prefetch" href="https://fonts.gstatic.com/">
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>
</head>
<body>
<p>hello</p>
</body>
</html>
3.Web 安全
XSS(Cross Site Scripting,跨站脚本攻击)
手段:黑客将 JS 代码插入到网页内容中,渲染时执行 JS 代码
XSS攻击场景:
- 我在网页发表一篇博客,其中内嵌 <script> 脚本
- 此脚本获取cookie,发送到我的服务器
- 这样的话有人浏览这篇文章,我就能获得这个人的 cookie 信息
- 所以如果页面随意执行不安全的 JS 代码,用户信息和网站将变得极不安全
XSS攻击预防:
- 比如上面场景我们可以替换特殊字符 如 < 变为 <
- 这样的话
<script>标签脚本直接变为<script>,而不会作为脚本执行 - 前后端最好都进行替换
& 替换为:&
< 替换为:<
> 替换为:>
” 替换为:"
‘ 替换为:'
/ 替换为:/
此外可以对 cookie 增加 http-only 属性使得通过 JS 不能访问
Vue 的 v-html 和 React 的 dangerouslySetInnerHTML 会有 XSS 攻击的风险
CSRF(Cross-site request forgery,跨站请求伪造)
手段:黑客诱导用户去访问另一个网站的接口,伪造请求
CSRF攻击场景:
- 你正在购物,看中某个某个商品,该商品 id 是100
- 付费接口是 xxx.com/pay?id=100,该接口没有任何 密码 和 token 验证
- 我是攻击者,我看中了一个商品,id 是 200
- 此时我向你发送一封邮件,邮件正文隐藏着
<img src=xxx.com/pay?id=200> - 你一查看邮件,就帮我购买了 id 是 200 的商品
- 正是你携带了该购物网站的 cookie 发送了购物请求,即使你是在其他页面请求的该网站
再简单点:
- 用户登录了 A 网站,有了 cookie
- 黑客诱导用户到 B 网站,并发起 A 网站的请求
- A 网站的 API 发现有 cookie ,认为是用户自己操作的
CSRF攻击预防:
- 严格控制跨域,如判断referrer (请求来源)
- 为 cookie 设置 SameSite ,禁止跨域传递 cookie
Set-Cookie: key1=val1; key2=val2; SameSite=Strict; - 使用 Post 接口
- 增加密码、短信验证码、指纹等验证
点击劫持(Click Jacking)
- 手段:诱导界面上蒙一个透明的iframe ,诱导用户点击
- 预防∶让iframe 不能跨域加载
- 点击劫持的原理:黑客在自己的网站,使用隐藏的
<iframe>嵌入其他网页,诱导用户按顺序点击
使用 JS 预防
if (top.location.hostname !== self.location.hostname) {
alert("您正在访问不安全的页面,即将跳转到安全页面!")
top.location.href = self.location.href
}
增加 http header X-Frame-Options:SAMEORIGIN ,让 <iframe> 只能加载同域名的网页
PS:点击劫持,攻击那些需要用户点击操作的行为。CSRF 不需要用户知道,偷偷完成
DDoS(Distributed denial-of-service 分布式拒绝服务)
通过大规模的网络流量淹没目标服务器或其周边基础设施,以破坏目标服务器、服务或网络正常流量的恶意行为
类似于恶意堵车,妨碍正常车辆通行
网络上的设备感染了恶意软件,被黑客操控,同时向一个域名或者 IP 发送网络请求。因此形成了洪水一样的攻击效果
由于这些请求都来自分布在网络上的各个设备,所以不太容易分辨合法性
DDoS 的预防:软件层面不好做,可以选择商用的防火墙,如阿里云 WAF
PS:阮一峰的网站就曾遭遇过 DDoS 攻击 www.ruanyifeng.com/blog/2018/0…
SQL 注入
普通的登录方式,输入用户名
zhangsan、密码123,然后服务端去数据库查询会执行一个 sql 语句
select * from users where username='zhangsan' and password='123',然后判断是否找到该用户如果用户输入的是用户名
' delete from users where 1=1; --,密码'123'那生成的 sql 语句就是
select * from users where username='' delete from users where 1=1; --' and password='123'这样就会把
users数据表全部删除防止 SQL 注入:服务端进行特殊字符转换,如把
'转换为\'
4.垃圾回收
-
全局环境变量以及闭包变量不会被回收
-
局部函数执行完内部变量会被回收
-
标记清除(现代Javascript采用标记清除)
-
从全局变量往下一层层查找,能找到的就标记,没有被标记引用的变量就是垃圾
-
当一个对象时这种可达状态时,它一定是存在于内存中的
-
-
引用计数(早期)
- 以“数据是否被引用”来判断要不要回收
- 创建引用这个对象 +1 , 别人引用也 +1 ,当此对象引用为 0 则会被清除
- 会有循环引用问题
更多请看这篇文章:垃圾回收 (javascript.info)
正常一个函数执行完,其中变量会被 JS 垃圾回收
function fn() {
const a = 'yunmu'
const obj = {
x: 100
}
}
fn()
某些情况下,变量是销毁不了的,因为可能会被再次使用
function fn() {
const obj = {
x: 100
}
window.obj = obj // 引用到了全局变量,obj 销毁不了
}
fn()
function genDataFns() {
const data = {} // 闭包,data 销毁不了
return {
get(key) {
return data[key]
},
set(key, val) {
data[key] = val
}
}
}
const { get, set } = genDataFns()
引用计数
// 对象被 a 引用
let a = {
b: {
x: 10
}
}
let a1 = a // 又被 a1 引用
let a = 0 // 不再被 a 引用,但仍然被 a1 引用
let a1 = null // 不再被 a1 引用
// 对象最终没有任何引用,会被回收
但这个算法有一个缺陷 —— 循环引用。例如:
function fn() {
const obj1 = {}
const obj2 = {}
obj1.a = obj2
obj2.a = obj1 // 循环引用,无法回收 obj1 和 obj2
}
fn()
标记清除
基于上面的问题,现代浏览器使用“标记-清除”算法。根据“是否是否可获得”来判断是否回收
定期从根(即全局变量)开始向下查找,能找到的即保留,找不到的即回收。循环引用不再是问题
5.内存泄漏
检测内存变化
可使用 Chrome devTools Performance 来检测内存变化
- 刷新页面,点击“GC”按钮
- 点击“Record”按钮开始记录,然后操作页面
- 操作结束,点击“GC”按钮,点击“结束”按钮,看分析结果
内存泄漏的场景
Vue举例子:
组件中有全局变量、函数、定时器、全局事件、自定义事件 ,在组件销毁时要记得清除
export default {
data() {
return {
intervalId: 0,
nums: [10, 20, 30]
}
},
methods: {
printNums() {
console.log(this.nums)
}
},
mounted() {
window.printNums = () => {
console.log(this.nums)
}
this.intervalId = setInterval(this.printNums, 200)
window.addEventListener('reisze', this.printNums)
event.on('event-key', this.printNums)
},
beforeUnmount() {
window.printNums = null
clearInterval(this.intervalId)
window.removeEventListener('reisze', this.printNums)
event.off('event-key', this.printNums)
},
}
扩展
- WeakMap WeakSet 弱引用,不会影响垃圾回收
// 函数执行完,obj 会被销毁,因为外面的 WeakMap 是“弱引用”,不算在内
const wMap = new WeakMap()
function fn() {
const obj = {
name: 'zhangsan'
}
// 注意,WeakMap 专门做弱引用的,因此 WeakMap 只接受对象作为键名(`null`除外),不接受其他类型的值作为键名。其他的无意义
wMap.set(obj, 100)
}
fn()
// 代码执行完毕之后,obj 会被销毁,wMap 中也不再存在。但我们无法第一时间看到效果。因为:
// 内存的垃圾回收机制,不是实时的,而且是 JS 代码控制不了的,因此这里不一定能直接看到效果。
// 函数执行完,obj 会被销毁,因为外面的 WeakSet 是“弱引用”,不算在内
const wSet = new WeakSet()
function fn() {
const obj = {
name: 'zhangsan'
}
wSet.add(obj) // 注意,WeakSet 就是为了做弱引用的,因此不能 add 值类型!!!无意义
}
fn()