一、从输入 URL 到页面加载显示完成发生了些什么?
这个问题涉及到的知识面非常广,区分度也比较高;所以,回答这个问题的基本原则是全面覆盖整个流程,擅长的点可以展开谈,对于前端来说,重点是谈好渲染过程。
第一步:Browser 进程中的 UI 线程——搜索 or URL?
如果是搜索的话,就将搜索关键字打包好URL请求搜索引擎;如果是请求站点,就进入到下一步。
第二步:UI 线程通知 Network 线程,进行请求加载
- 获取 IP 与 端口
这个过程涉及到 URL 解析、DNS解析。拓展:URL的构成部分
拓展:DNS解析过程
- 建立TCP、TSL连接
- 收到301的话,回到1重新再来
- 设置UA等信息,发送Get请求
- Web Server上的应用处理请求
- 读取 Response,分析数据类型
- 安全检查
拓展:黑白名单过滤、跨站攻击检查
- 通知UI数据准备就绪
到第二步结束时,此次会话加入历史,前进后退按钮已经可以使用了,导航到此结束。
第三步:Browser 进程中的 UI 线程通过进程间的通信通知 Renderer 进程进行渲染
-
主线程(main thread)
-
解析HTML
构建 DOM,边解析DOM边加载子资源,注意 JS 会阻塞解析(async/defer 除外)。
解析完毕后,触发 DOMContentLoaded 事件;等所有子资源都加载完毕以后触发 Loaded 事件。扩展:关于子资源加载还涉及到 CDN、资源加载的优先级等;此外,还可能涉及到一些特殊的标签,比如meta类的。
-
计算CSS
构建 CSSOM,通过合并 DOM 和 CSSOM 生成渲染树,从而得到页面上所需要的节点。 -
布局
基于渲染树,计算每个节点的位置和大小,从而得到布局树。
-
-
Raster 线程 & Compositer 线程
- 绘制
- 创建绘制记录,确定绘制的顺序
- 将页面拆分图层,构建图层树(Layer Tree)
- 复合
复合线程像素化图层,创建一个复合帧
- 绘制
注意:
上面的渲染过程中理论地描述了一个复合帧的产生。实际过程是反复渲染(生成帧)。就每一帧而言,其生命周期中还发生了一些事情,比方说:
- 在解析HTML之前,会执行 rAF 中注册的任务
- 在复合帧结束之后,如果存在空闲时间(该帧小于16ms),那么就会执行 rIC 里面注册的任务
就所有帧的时间线来说,存在一些比较有特殊意义的帧,用来作为性能指标,比如说:
- FCP
- LCP
- Total Blocking Time
描述 FCP 与 Time to Interactive 之间的阻塞时间,这段时间用户可以看到一些有意义的内容,但是无法与页面进行交互。
二、什么是首屏?怎么优化?
(一)首屏的概念及其重要性
首屏就是打开网站后的第一屏内容。
Web 增量加载的特点决定了首屏性能不会完美,但首屏又非常重要:
- 过长的白屏影响用户体验和留存
- 首屏决定了初次映像
(二)首屏优化的衡量指标
首屏有三个关键阶段:
- 发生了什么?
用户此时经历的是白屏已经屏幕开始有一些内容,用户由疑问到确定这个网站在运行。 - 有哪些内容呢?
用户开始看到一些有意义的内容。 - 可以用了吗?
这时候网站可以进行交互了。
这些关键阶段都有具体的衡量指标和标准,参考如下:
(三)首屏优化的具体措施
资源体积太大?
资源压缩混淆、代码拆分、Tree shaking、传输压缩、HTTP2、CDN、缓存
首页内容太多?
路由、组件、内容进行懒加载,预渲染/SSR,Inline CSS
加载顺序不合适?
pretch,preload
非技术角度还可以考虑,加载动画、骨架图进行体验优化。
三、JavaScript 内存管理
(一)JS 是怎么管理内存的?
变量创建时自动分配内存,不使用时“自动”释放内存——GC(垃圾回收);也就是所谓的“自动分配,自动回收”。
内存泄露问题主要出现在自动回收环节,就是怎么确定不再需要使用的内存,但基本上都是近似实现,而且已经被证明无法被解决;目前的自动回收都是通过判断变量是否还能够被访问到。
具体实现有两种:
- 引用计数法
由于循环引用的问题,2012年后已被淘汰 - 标记清除法
GC root 是否能触达变量,将不能触达的标记,等待下次 GC 统一回收。也存在局限性,例如:
上述情况下,object.b 没有被使用,但是也无法被回收。const object = {a: new Array(1000), b: new Array(2000)} setInterval(() => console.log(object.a), 1000)
(二)什么情况下会造成内存泄露?
两种变量:
- 局部变量,函数执行完,没有闭包引用,就会被标记回收
- 全局变量,直至浏览器卸载页面时释放
(三)代码层面如何避免内存泄露?
- 避免意外的全局变量的产生
function accidentialGlobal() { leak = 'leak1'; this.leak2 = 'leak2'; } accidentialGlobal(); window.leak1; window.leak2;
- 避免反复运行引发大量闭包
var store; function outer() { var largeData = new Array(1000000) var preStore = store function inner() { if (preStore) return largeData } return function() {} } erval(function() { store = outer() }, 10)
- 避免脱离的 DOM 元素
function createElement() { const div = document.createElement('div') div.id = 'detached' return div } const ditachedDiv = createElement() document.body.appendChild(ditachedDiv) function deleteElement() { document.body.removeChild(document.getElementById('detached')) } deleteElement() // 脱离了文档不意味着该变量不占用内存, 从而造成泄漏