导航&渲染、首屏优化、JavaScript内存管理

一、从输入 URL 到页面加载显示完成发生了些什么?

这个问题涉及到的知识面非常广,区分度也比较高;所以,回答这个问题的基本原则是全面覆盖整个流程,擅长的点可以展开谈,对于前端来说,重点是谈好渲染过程。

第一步:Browser 进程中的 UI 线程——搜索 or URL?

如果是搜索的话,就将搜索关键字打包好URL请求搜索引擎;如果是请求站点,就进入到下一步。

第二步:UI 线程通知 Network 线程,进行请求加载

  1. 获取 IP 与 端口
    这个过程涉及到 URL 解析、DNS解析。

    拓展:URL的构成部分
    URL构成

    拓展:DNS解析过程

  2. 建立TCP、TSL连接

    拓展:HTTP、HTTPS、TCP/IP、三次握手四次挥手过程?

  3. 收到301的话,回到1重新再来
  4. 设置UA等信息,发送Get请求
  5. Web Server上的应用处理请求
  6. 读取 Response,分析数据类型
  7. 安全检查

    拓展:黑白名单过滤、跨站攻击检查

  8. 通知UI数据准备就绪

到第二步结束时,此次会话加入历史,前进后退按钮已经可以使用了,导航到此结束。

第三步:Browser 进程中的 UI 线程通过进程间的通信通知 Renderer 进程进行渲染

  • 主线程(main thread)

    1. 解析HTML
      构建 DOM,边解析DOM边加载子资源,注意 JS 会阻塞解析(async/defer 除外)。
      解析完毕后,触发 DOMContentLoaded 事件;等所有子资源都加载完毕以后触发 Loaded 事件。

      扩展:关于子资源加载还涉及到 CDN、资源加载的优先级等;此外,还可能涉及到一些特殊的标签,比如meta类的。

    2. 计算CSS
      构建 CSSOM,通过合并 DOM 和 CSSOM 生成渲染树,从而得到页面上所需要的节点。

    3. 布局
      基于渲染树,计算每个节点的位置和大小,从而得到布局树。

  • Raster 线程 & Compositer 线程

    1. 绘制
      • 创建绘制记录,确定绘制的顺序
      • 将页面拆分图层,构建图层树(Layer Tree)
    2. 复合
      复合线程像素化图层,创建一个复合帧

注意:
上面的渲染过程中理论地描述了一个复合帧的产生。实际过程是反复渲染(生成帧)。

就每一帧而言,其生命周期中还发生了一些事情,比方说:

  1. 在解析HTML之前,会执行 rAF 中注册的任务
  2. 在复合帧结束之后,如果存在空闲时间(该帧小于16ms),那么就会执行 rIC 里面注册的任务

就所有帧的时间线来说,存在一些比较有特殊意义的帧,用来作为性能指标,比如说:

  1. FCP
  2. LCP
  3. Total Blocking Time
    描述 FCP 与 Time to Interactive 之间的阻塞时间,这段时间用户可以看到一些有意义的内容,但是无法与页面进行交互。

二、什么是首屏?怎么优化?

(一)首屏的概念及其重要性

首屏就是打开网站后的第一屏内容。

Web 增量加载的特点决定了首屏性能不会完美,但首屏又非常重要:

  1. 过长的白屏影响用户体验和留存
  2. 首屏决定了初次映像

(二)首屏优化的衡量指标

首屏有三个关键阶段:

  1. 发生了什么?
    用户此时经历的是白屏已经屏幕开始有一些内容,用户由疑问到确定这个网站在运行。
  2. 有哪些内容呢?
    用户开始看到一些有意义的内容。
  3. 可以用了吗?
    这时候网站可以进行交互了。

首屏优化的衡量指标

这些关键阶段都有具体的衡量指标和标准,参考如下:
首屏衡量指标及其标准

(三)首屏优化的具体措施

资源体积太大?

资源压缩混淆、代码拆分、Tree shaking、传输压缩、HTTP2、CDN、缓存

首页内容太多?

路由、组件、内容进行懒加载,预渲染/SSR,Inline CSS

加载顺序不合适?

pretch,preload

非技术角度还可以考虑,加载动画、骨架图进行体验优化。

三、JavaScript 内存管理

(一)JS 是怎么管理内存的?

变量创建时自动分配内存,不使用时“自动”释放内存——GC(垃圾回收);也就是所谓的“自动分配,自动回收”。

内存泄露问题主要出现在自动回收环节,就是怎么确定不再需要使用的内存,但基本上都是近似实现,而且已经被证明无法被解决;目前的自动回收都是通过判断变量是否还能够被访问到。

具体实现有两种:

  1. 引用计数法
    由于循环引用的问题,2012年后已被淘汰
  2. 标记清除法
    GC root 是否能触达变量,将不能触达的标记,等待下次 GC 统一回收。也存在局限性,例如:
    const object = {a: new Array(1000), b: new Array(2000)}
    setInterval(() => console.log(object.a), 1000)
    
    上述情况下,object.b 没有被使用,但是也无法被回收。

(二)什么情况下会造成内存泄露?

两种变量:

  1. 局部变量,函数执行完,没有闭包引用,就会被标记回收
  2. 全局变量,直至浏览器卸载页面时释放

(三)代码层面如何避免内存泄露?

  1. 避免意外的全局变量的产生
    function accidentialGlobal() {
    	leak = 'leak1';
    	this.leak2 = 'leak2';
    }
    accidentialGlobal();
    window.leak1;
    window.leak2;
    
  2. 避免反复运行引发大量闭包
    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)
    
  3. 避免脱离的 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()
    // 脱离了文档不意味着该变量不占用内存, 从而造成泄漏
    

拓展:
JavaScript 内存调试技巧与泄露分析

用户头像
登录后发表评论