浏览器等运行环境面试知识体系汇总

804 阅读7分钟

引子:写给自己的面试体系笔记,如有概念性错误,烦请指出。

1.加载页面和渲染过程

浏览器从加载页面到渲染页面的过程

加载过程:

  1. 浏览器网络请求线程的开启

  2. 浏览器根据 DNS 服务器得到域名的 IP 地址

  3. 在 TCP IP 通道连接下 向这个 IP 的机器发送 HTTP 请求

  4. 服务器收到、处理并返回 HTTP 请求

  5. 浏览器得到返回对应内容

浏览器渲染过程:

  1. 根据 HTML 结构生成 DOM 树
  2. 根据 CSS 生成 CSSOM
  3. 将 DOM 和 CSSOM 整合形成 RenderTree
  4. 根据 RenderTree 开始布局和绘制
  5. 遇到<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 渲染
  • 优化体验:骨架屏 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攻击预防:

  • 比如上面场景我们可以替换特殊字符 如 < 变为 &lt;
  • 这样的话 <script> 标签脚本直接变为 &lt;script&gt;,而不会作为脚本执行
  • 前后端最好都进行替换
& 替换为:&amp;
< 替换为:&lt;
> 替换为:&gt;
” 替换为:&quot;
‘ 替换为:&#x27;
/ 替换为:&#x2f;

此外可以对 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> 只能加载同域名的网页

image-20220309155003464

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()