前言
本文旨在深度解析一个前端项目,如何从源代码经过打包构建,到部署上线,最终在浏览器中渲染呈现给用户的完整实现。
一,打包构建
所谓打包就是将前端项目通过一些打包构建工具,类似于webpack,vite 等等将其通过解析,转译,优化,合并一系列流程将其变成浏览器能够高效加载和执行的静态资源。
下面是我以webapck为例打包vue项目打包过程
1,模块转换和转译
webpack的打包主要是调用各种Loader 对模块进行转换,其中有两个重要的Loader
vue-loader
- 负责将
.vue文件转换为描述符对象,并提取文件中的<template>、<script>、<style>三部分。 - 模板编译:将
<template>中HTML 标签字符串转换为模板AST ( 抽象语法树 描述标签,属性,文本的节点结构 ) 然后经过再次优化(例如静态标记),最终将AST转换为render渲染函数字符串(如return h('div',msg))这样 - 将
<scripts>脚本文件部分提取然后交给Babel-loader去处理
Babel-loader
- 解析:Babel解析器 将TS等源码字符串解析成AST(注意这个与上面提到的AST不一样,这个AST描述的是 js的语法结构 ,如变量声明,箭头函数等)
- 转换:遍历 AST 对节点进行增删改查操作,主要是进行语法降级,例如转换箭头函数为普通函数等的操作
- 生成:转换完成之后,由 Babel 生成器将修改后的AST重新生成广泛兼容的ES5代码字符串
2,依赖分析和打包封装
-
构建依赖图: 从入口文件开始,根据import/require 语句静态分析所有模块的依赖关系,从而形成完整的依赖图。
-
打包与优化:
- Tree Shaking(摇树优化): 基于ES6静态模块静态结构,移除未被使用的导出代码
- Code Splitting(代码分割): 根据动态导入或配置,将代码拆分成多个Bundle,实现按需加载
- 模块合并: 根据依赖图将成千上万个转换后的模块封装成一个或几个bundle文件中,减少http请求数量
-
产物生成
输出最终打包之后的产物
app.[hash].js(应用主逻辑)vendor[hash].js(三方依赖),style.[hash].css(提取的样式文件) 等等的文件,然后通过HtmlWebpackPlugin等插件更新index.html 将最终的带有哈希值的文件注入到<script>和<link>标签中
上面这部分主要是依照webpack和vue 文件为例,说明了项目打包构建的过程和其最终生成的产物,下来主要介绍将构建完成的前端静态资源文件部署到服务器上,是如何通过域名访问的过程。
二,导航加载
当用户在浏览器地址栏输入URL并按下回车后,以下关键流程在浏览器中启动:
-
当用户在浏览器输入网址访问时,最先由浏览器进程UI线程处理这个用户导航,并将url 转交给浏览器进程内的网路线程。
-
网络线程拿到url之后首先会检查本地的缓存,如果没有则会发起DNS解析,将域名转换为IP 地址。之后便是建立TCP三次握手建立连接,若是Https还需要TLS 握手,发送HTTP 请求并接受响应,Nginx 服务器接受请求并返回
Index.html文件及其他静态资源。 ** -
网络线程会先检查请求的响应头
content-type : text/html看看他是否为这个类型,如果是则确认这是一个可渲染的文档,然后进行渲染流程 -
浏览器会创建一个渲染进程 (同一站点(相同协议,相同域名,端口)的多个标签可能会共享一个渲染进程)
-
渲染进程接收到数据之后会同网络线程建立数据传输通道,网络集成并不等待整个HTML下载完,而是将接收到的原始字节流,实时的,分块的通过传输通道推送给渲染进程
三,💡 解析,执行,渲染(浏览器渲染原理)
这是浏览器内核最复杂的,最核心的工作阶段
渲染主线程
1,解析HTML构建DOM 树
渲染主线程收到从流式接口中读取字节流,根据编码将其转换成字符流,
HTML解析器将字符流进行语法分析,拆分成令牌(tokens)
然后遵循HTML规范,将令牌转换为DOM节点对象,节点被依次添加到树中,并逐步构建成一棵完整的DOM树。
预加载扫描器(关键优化器): 一个独立的扫描器会快速“偷看”后续的Token,发现
<link>标签和<scripts>等外部资源链接,立即通知浏览器进程中的网络进程去并行请求这些资源。
2,解析CSS
主线程解析到<link> 或 <style> 标签时,会同步地(如果是内联)或异步地(如果是外链且已预加载)加载并解析CSS,构建 CSSOM树。
注意:CSS不会阻塞DOM的解析,但会阻塞DOM的渲染(因为需要CSSOM来构建渲染树)。
💡💡💡 执行JS代码(V8引擎工作机制)
当解析到 标签时,会暂停HTML解析(除非标记为async 或者 defer)立即加载并执行JS代码
V8引擎执行流程
-
词法分析:将js 源码字符串转换为AST(包含变量,函数的语法树)
-
解释执行:Ignition解释器将AST转换为字节码并快速执行
-
优化编译:TurboFan编译器在运行时收集热点函数(重复执行),将其编译为高度优化的机器码,后续直接执行机器码,性能大幅提升。
-
系统交互:执行中遇到Web API调用,通过桥接传给浏览器其他模块处理;其回调任务通过事件循环机制排队,在适当时机被推回V8的调用栈执行。
-
内存管理:GC垃圾回收机制自动管理堆内存,通过标记-清除算法、分新生代和老生代回收等算法回收不再使用的对象。
3,构建渲染树(Render Tree)
渲染树是DOM树和CSSOM树的结合,只包含可见节点(不包括display:none的元素)
4,布局(Layout)
计算渲染树中每个节点在视口内的确切位置和尺寸(像素值),这个过程也称之为重排(Reflow)
5,分层(Layering)
渲染主线程会根据元素的层叠上下文、3D变换等属性,将页面划分为多个图层。分层旨在将变动隔离在单个图层内,提升渲染效率。 其好处在于某一个层改变后,只会对该层进行处理而不影响其他层,从而提升效率
6,绘制(Paint)
渲染主线程会负责生成每个图层的绘制指令集,绘制指令集包含了元素的位置,大小,颜色,等信息,最后将渲染树中的每个元素转换为屏幕上的实际像素,渲染主线程的工作就到此为止,后续的步骤会由其他线程完成 。渲染主线程会将每个图层的绘制信息提交给合成线程
合成线程
7,分块(Tiling)
分块是指将页面中的图层划分为多个小块区域,这个过程由合成线程负责
8,光栅化(Rasterization)
合成线程将每个图层分割成更小的图块 进行光栅化时 通常是采取优化策略将优先处理靠近视口的块,因为他们最先被用户看到,可见度高,优先处理这些块可提高页面的显示速度和用户体验
9,合成(Composition)
将各个图层合成为完整的页面,浏览器会将经过光栅化处理的各个图层按照一定的顺序进行组合,并交给 GPU 进⾏,利用硬件加速提高性能。
问题探讨
1,浏览器多进程架构的好处
稳定性: 进程隔离,单页面崩溃不影响其他页面
安全性:使用沙箱隔离,无法访问权限,无法访问文件系统,执行系统命令
性能方面:使用多核cpu 并行处理
2,CSS 不会阻塞 HTML 解析的根本原因
解析过程中遇到 CSS 解析 CSS,遇到 JS 执行 JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。
如果主线程解析到link位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。
3,JS 会阻塞 HTML 解析的根本原因
如果主线程解析到script位置非(async/defer),会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。
4,transform 效率高的原因
因为transform的变换逻辑完全在合成线程中处理,不会触发渲染主线程的任何工作(如布局计算、样式重算或绘制指令生成),它影响的只是渲染流程的最后一个绘制阶段(也称合成绘制阶段)。
相比之下,修改left、width等属性会直接触发布局阶段的重排,进而引发重绘和重新光栅化,这些过程都需要渲染主线程参与浪费性能。