客户端容器
课程介绍
本节课程主要着重介绍浏览器的架构以及运行原理,并以一道八股文为例,讲解在Chrome浏览器里,网页是如何加载并且渲染成我们所见的画面。通过对本节课的学习,你还能了解到在浏览器里JS引擎和渲染引擎如何协同工作,如何从多个角度优化前端的性能体验。再以webview容器为扩展,带你认识一些常见的跨端方案。
课程重点
-
浏览器架构
- 浏览器架构变迁
- 浏览器各架构对比
- 多进程架构介绍
-
渲染进程
- 渲染进程多线程架构
- JS引擎VS渲染引擎
- 多线程工作流程
-
chrome运行原理
- 主进程工作流程
- 渲染进程工作流程
- 浏览器性能优化
-
跨端容器
- 跨端的由来
- 常见的跨端方案
浏览器架构
浏览器架构变迁
- 单进程架构: 所有模块运行在同一个进程里,包含网络、插件、JavaScript运行环境等
- 多进程架构: 主进程、网络进程、渲染进程、GPU进程、插件进程
- 面向服务架构: 将原来的UI、数据库、文件、设备、网络等,作为一个独立的基础服务
现在市面上大多数浏览器都是多进程架构,各进程相互独立,安全性比较高;
面向服务架构实际上也是多进程架构,进程之间访问需要事先定义好接口,通过api来进行基础交互
浏览器各架构对比
多进程架构介绍
- 问题思考
- 为什么会有单进程架构:节约资源(例如架构)和内存(早期内存昂贵)
- 面向服务架构是否会替代多进程架构:性能较好的机器为了节约资源和优化就会选择面向服务
渲染进程
常见浏览器内核
渲染进程多线程架构
内部是多线程实现的,主要负责页面渲染,脚本执行,时间处理,网络请求等
JS引擎VS渲染引擎
- 解析执行JS
- XML解析生成渲染树,显示在屏幕
- 桥接方式通信
V8将高频函数转成机器码,下次可以直接执行,起到优化作用
JS引擎和渲染引擎是相互独立的,虽然JS代码执行是很快的,但是在操作dom的时候通过bridge桥接通信会造成延时
多线程工作流程
- 网络线程负责加载网页资源
- JS引擎解析JS脚本并且执行
- JS解析引擎空闲时,渲染线程立即工作
- 用户交互、定时器操作等产生回调函数放入任务队列中
- 事件线程进行事件循环,将队列里的任务取出交给JS引擎执行
const now = Date.now() setTimeout(() => { console.log('time10', Date.now() - now) }, 10) setTimeout(() => { console.log('time30', Date.now() - now) }, 30) while(true) { if (Date.now() - now >= 20) break } console.log(Date.now() - now) // 输出: 20 30 50
执行顺序:while语句--知道等于20ms退出循环--》最底下的语句,输出20ms-->第一个延时器,等待10ms,控制台输出30ms----->第二个延时器,前一个已经等待了10ms,此时只需等待20ms,控制台输出(20+30)50ms
chrome运行原理
如何展示页面
浏览器地址输入url后发生了什么? 请你谈谈网站是如何进行访问的(改动Windows底层文件)
1. 输入域名;回车
2. 先检查hosts配置文件里面有没有www.xsn.com域名的映射(一个IP地址可以对应多个域名,一个域名对应一个IP)
2.1. 有则直接返回对应的IP地址127.0.0.1,这个地址有我们需要访问的web程序,可以直接访问
2.2. 没有则去DNS服务器(管理所有域名的地方)找
- 对于web程序的访问,浏览器主进程会处理输入,然后开始导航,导航后去浏览器读取响应,再寻找渲染进程去渲染这次加载,拿到html/css/js脚本后,构建dom,cssom,由这两者构成渲染树,之后开始布局,再绘制图层,最后合成,进行展示,展示完成,返回浏览器主进程。(比如输入index.html,导航到该html的位置,读取浏览器响应回来的html、css文件,交由渲染进程渲染页面,包括加载html文件和图片、CSS样式、JS脚本)
输入处理
- 用户Url框输入内容的后,UI线程会判断输入的是一个URL地址呢,还是一个query查询条件
- 如果是URL,直接请求站点资源
- 如果是query,将输入发送给搜索引擎
开始导航
- 当用户按下回车,UI线程通知网络线程发起一个网络请求,来获取站点内容
- 请求过程中,tab处于loading状态(转圈圈)
读取响应
- 网络线程接收到HTTP响应后,先检查响应头的媒体类型 (MIME Type)
- 如果响应主体是一个HTML文件,浏览器将内容交给渲染进程处理
- 如果拿到的是其他类型文件,比如Zip.exe等,则交给下载管理器处理
寻找渲染进程
- 网络线程做完所有检查后,会告知主进程数据已准备完毕,主进程确认后为这个站点寻找一个渲染进程
- 主进程通过IPC消息告知渲染进程去处理本次导航
- 渲染进程开始接收数据并告知主进程自己已开始处理,导航结束,进入文档加载阶段
渲染进程工作流程
资源加载
- 收到主进程的消息后,开始加载HTML文档
- 除此之外,还需要加载子资源,比如一些图片,CSS样式文件以及JavaScript脚本
构建渲染树
- 构建DOM树,将HTML文本转化成浏览器能够理解的结构
- 构建CSSOM树,浏览器同样不认识CSS,需要将CSS代码转化为可理解的CSSOM
- 构建渲染树,渲染树是DOM树和CSSOM树的结合
页面布局
- 根据渲染树计算每个节点的位置和大小
- 在浏览器页面区域绘制元素边框
- 遍历渲染树,将元素以盒模型的形式写入文档流
页面绘制
- 构建图层:为特定的节点生成专用图
- 绘制图层: 一个图层分成很多绘制指令,然后将这些指令按顺序组成一个绘制列表,交给合成线程
- 合成线程接收指令生成图块
- 栅格线程将图块进行栅格化(栅格化其实就是把图块转成绘图,具体成一个个像素点,会使用GPU加速栅格化过程)
- 展示在屏幕上(第四步调用显卡就可以展示了)
浏览器性能优化
前端性能performance
- 时间都花在哪:解析,等待资源加载
- 什么情况下卡顿:接口加载时间太长,来不及刷新
首屏优化
- 压缩、分包、删除无用代码:通过压缩代码、分包加载和删除无用代码等技术,可以减小页面的体积,加快页面的加载速度。
- 静态资源分离:将页面中的静态资源(如CSS、JavaScript和图像等)与HTML文档分离,可以使得浏览器可以并行加载这些资源,从而提高页面的加载速度
- JS脚本非阻塞加载:将JS脚本异步加载,可以减少页面的渲染阻塞,从而提高页面的加载速度。可以使用
defer和async等属性来实现JS的非阻塞加载 - 缓存策略:合理地设置缓存策略,可以减少对服务器的请求,加快页面的加载速度。可以使用HTTP响应头中的
Cache-Control和Expires等属性来设置缓存策略 - SSR:服务器端渲染(Server Side Rendering)可以在服务器端生成HTML文档,减少客户端渲染的工作量,从而提高页面的加载速度。SSR适用于复杂的单页面应用或对SEO有要求的应用
- 预置loading、骨架屏: 在页面加载过程中,可以预置一个loading动画或骨架屏,以提高用户体验。这些技术可以在页面加载完成之前,先显示一些占位元素,给用户一个等待的感觉,从而减少用户等待的焦虑和不安
渲染优化
- GPU加速:将复杂的图形处理任务交给GPU来处理,可以加快页面的渲染速度。可以使用CSS3的
transform和opacity等属性来开启GPU加速 - 减少回流、重绘:回流和重绘是影响页面性能的主要因素之一。可以通过避免使用影响布局的属性、批量修改DOM元素等技术来减少回流和重绘操作
- 离屏染染:离屏渲染是将页面中的部分内容在单独的图层中进行渲染,从而减少对主渲染线程的阻塞。可以使用CSS3的
transform和position等属性来开启离屏渲染。 - 懒加载:将页面中的非必要资源(如图片和视频等)延迟加载,可以加快页面的加载速度。可以使用
Intersection Observer和Lazyload等技术来实现懒加载
JS优化
- 防止内存泄漏:
-
有可能出现内存泄漏的场景
- 全局变量:全局变量会一直存在于内存中,直到程序结束才会被释放。如果程序中定义了大量的全局变量,就会导致内存占用过多,从而导致内存泄漏。
- 闭包:闭包会在函数中保存局部变量和参数,如果函数执行后,闭包中的变量没有被释放,就会导致内存泄漏。为了避免内存泄漏,应该合理使用闭包,并注意释放不需要的变量。
- 循环引用:循环引用是指两个或多个对象之间相互引用,形成了一个死循环,导致内存无法释放。为了避免循环引用,应该及时释放不需要的引用,并使用垃圾回收机制来自动释放内存。
- 定时器和事件监听器:定时器和事件监听器会持续占用内存,直到被清除或被解除绑定。如果程序中存在大量的定时器和事件监听器,就会导致内存占用过多,从而导致内存泄漏。
- DOM节点:DOM节点也会占用内存空间,如果程序中存在大量的DOM节点,就会导致内存占用过多,从而导致内存泄漏。为了避免内存泄漏,应该及时清除不需要的DOM节点
-
内存泄漏会导致不必要的内存占用和程序崩溃。可以使用
let和const关键字声明变量,避免变量污染和内存泄漏
- 循环尽早break:在循环中,如果已经找到了需要的结果,可以使用
break语句尽早结束循环,避免无用的迭代和计算 - 合理使用闭包:闭包可以在函数中保存局部变量和参数,避免全局变量的污染和泄漏。但是,如果使用不当,也会导致内存泄漏和性能下降
- 减少Dom访问:DOM操作是JavaScript性能的一个瓶颈。可以使用缓存和批量操作等技术来减少DOM访问次数,从而提高JavaScript的性能
- 防抖、节流:防抖是在一定时间内只执行一次目标函数,而节流是在一定时间内控制目标函数执行次数。它们都可以有效地减少函数执行频率,降低性能开销,提高用户体验。防抖和节流是用来控制函数调用频率的技术。可以使用
setTimeout和requestAnimationFrame等API来实现防抖和节流(或者用第三方库也行) - Web Workers:Web Workers是一种在后台线程中执行JavaScript代码的技术。可以将耗时的计算任务和数据处理等操作放到Web Workers中执行,避免阻塞主线程,提高页面的响应速度
跨端容器
跨端的由来
为什么需要跨端:
- 开发成本低、效率高:使用跨端技术,开发者只需编写一份代码,就可以在多个平台(如iOS、Android和Web)上运行。这可以减少开发和维护的工作量,节省时间和资源。同时,开发团队可以更快地推出新功能和修复问题,因为他们只需关注一份代码库。
- 一致性体验: 跨端开发可以确保在不同平台上提供一致的用户体验。使用跨端技术,开发者可以更容易地保持应用的外观和功能一致,无论用户在什么设备上使用。这有助于提高用户满意度和用户留存率。
- 前端开发生态:跨端开发受益于强大的前端生态系统。许多流行的前端框架和库,如React Native、Flutter和Ionic,都支持跨端开发。这些工具为开发者提供了丰富的资源和丰富的社区支持,帮助他们更轻松地实现跨端功能。
常见的跨端方案
跨端容器-WebView
- Webview,即网页视图,用于加载网页Url,并展示其内容的控件
跨端容器中的WebView是一种在移动应用中嵌入网页内容的组件。WebView允许开发者将HTML、CSS和JavaScript等Web技术直接嵌入到移动应用中,从而实现跨平台的应用开发。通过WebView,可以在原生应用中展示网页内容,同时为开发者提供了一些与原生功能交互的能力
- 可以内嵌在移动端App内,实现前端混合开发,大多数混合框架都是基于Webview的二次开发;比如lonic、Cordova
跨端容器-常用WebView分类
常用webview,Android,IOS、国产Android
WebView(Android):这是Android平台的原生WebView组件,用于在Android应用中加载并显示Web内容。根据Android版本和设备制造商的不同,WebView的表现可能会有所差异。这可能导致一些兼容性和性能问题。
X5 WebView(腾讯X5内核):这是由腾讯公司推出的一种WebView解决方案,用于解决Android系统上WebView的碎片化问题。X5内核基于腾讯QQ浏览器的内核,提供了更稳定、更高性能的WebView组件。它可以在各种Android设备和系统版本上提供一致的表现,减少兼容性问题。
UIWebView(iOS,已弃用):这是iOS平台的原生WebView组件,用于在iOS应用中加载并显示Web内容。UIWebView自iOS 2.0开始引入,但在iOS 8.0中被WKWebView取代。自iOS 12.0以来,UIWebView已被官方弃用,不再推荐使用。
WKWebView(iOS):这是iOS平台上的新一代WebView组件,取代了已弃用的UIWebView。WKWebView自iOS 8.0开始引入,它具有更好的性能和更丰富的功能,如支持多进程、JavaScript性能改进等。苹果公司推荐开发者使用WKWebView来在iOS应用中加载并显示Web内容。
跨端容器-使用WebView优势
- 一次开发,处处使用,学习成本低
- 随时发布,即时更新,不用下载安装包
- 移动设备性能不断提升,性能有保障
- 通过JSBridge和原生系统交互,实现复杂功能
WebView的主要优点包括: 跨平台兼容性,代码重用,简化更新过程(随时发布,即时更新,不用下载安装包),通过JSBridge与原生系统交互,实现更复杂的功能。
局限:性能下降,原生功能访问限制(可能需要编写额外代码),用户体验不一致有差异。
跨端容器-WebView使用原生能力
- Javascript 调用Native
- API注入: Native获取Javascript环境上下文,对其挂载的对象或者方法进行拦截
- 使用Webview URL Scheme 跳转拦截
- IOS上 window.webkit.messageHandler 直接通信
- Native 调用 Javascript
- 直接通过webview 暴露的 API 执行JS代码
- IOS webview.stringByEvaluatingJavaScriptFromString
- Android webview.evaluateJavascript
跨端容器-WebView<->Native 通信
- JS环境中提供通信的JSBridge
- Native 端提供 SDK 响应JSBridge 发出的调用
- 前端和客户端分别实现对应功能模块
JSBridge是一种在WebView中实现原生与JavaScript之间通信的技术
回调后删除回调函数,防止缓存越来越大
跨端容器-小程序
- 微信、支付宝、百度小程序、小米直达号
- 渲染层-webview
- 双线程,多webview架构
- 数据通信,Native转发
跨端容器-React Native/WeeX
- 原生组件渲染
- React/Vue框架
- virtual dom
- JSBridge
0. React Native: React Native是Facebook开发的一个开源框架,它允许开发者使用React和JavaScript编写原生移动应用。React Native的主要特点是"Learn once, write anywhere"(学习一次,随处编写),这意味着开发者可以在不同平台(如iOS和Android)上使用相同的技术栈构建原生应用。
React Native的优势包括:
代码复用:React Native允许开发者在多个平台上复用大部分代码,从而减少开发时间和成本。
热重载:React Native支持热重载功能,允许开发者在不重新编译整个应用的情况下查看代码更改的效果,提高开发效率。
原生性能:虽然React Native使用JavaScript编写,但它将JavaScript代码转换为原生组件,从而提供了接近原生应用的性能。
丰富的生态系统:React Native拥有庞大的社区支持,开发者可以利用许多第三方库和组件来加速开发过程。
- Weex: Weex是由阿里巴巴开发的一个开源框架,它允许开发者使用Vue.js和JavaScript编写原生移动应用。Weex的目标是实现"Write once, run everywhere"(编写一次,随处运行),即使用一套代码构建多个平台的原生应用。
Weex的优势包括:
- 代码复用:类似于React Native,Weex也支持在不同平台上复用代码,从而降低开发成本。
- 原生渲染:Weex将Vue.js组件转换为原生组件进行渲染,从而实现了较高的性能。
- 插件系统:Weex提供了一个丰富的插件系统,开发者可以使用这些插件来轻松实现原生功能,如地图、支付等。
- 模块化:Weex允许开发者将应用拆分为多个模块,这有助于实现高度模块化的开发过程,提高代码可维护性。
跨端容器-Lynx
- Vue:Vue.js是一种渐进式JavaScript框架,它使开发者能够轻松地构建可扩展、高性能的应用。基于Vue.js的设计原则,Lynx可以提供简洁、模块化的代码结构,以及良好的开发体验
- JS Core / V8:Lynx选择JS Core(iOS平台)或V8(Android平台) 作为其JavaScript引擎。这意味着Lynx应用在运行时,JavaScript代码将在高性能的JavaScript引擎中执行。通过使用这些优秀的JavaScript引擎,Lynx能够确保应用在不同平台上具有稳定、高性能的运行表现
- JSBinding(高效的JSBridge):Lynx使用JSBinding技术实现JavaScript与原生代码之间的通信。这种技术允许JavaScript直接调用原生方法,并使原生代码能够执行JavaScript回调。通过JSBinding,Lynx实现了高效的原生与JavaScript之间的通信,降低了性能损失
- Native Ul / Skia:Lynx使用Native UI组件和Skia作为其渲染引擎。Native UI组件意味着Lynx应用在运行时,界面将使用原生组件进行渲染。这可以确保应用具有接近原生应用的性能和用户体验。同时,Lynx还采用了Skia图形库,它是一种高性能的2D图形渲染引擎,用于绘制图形和文本。Skia使Lynx应用在渲染复杂界面时能够保持流畅的帧率和高质量的视觉效果
跨端容器-Flutter
Flutter是Google开发的一个开源UI工具包,旨在为开发者提供一种构建优美、高性能的跨平台应用的解决方案。Flutter具有一些独特的特点,包括基于Widget的设计、Dart VM以及使用Skia图形库
- wideget
- dart vm
- skia图形库
wideget
- 在Flutter中,所有UI元素都被称为Widget。Widget是Flutter应用的基本构建块,它们可以嵌套、组合以及自定义,从而创建复杂的用户界面。Flutter提供了丰富的预制Widget,如文本、按钮、列表等,开发者可以直接使用这些Widget,也可以通过组合和扩展它们来构建自定义的Widget
dart vm
- Flutter使用Dart语言进行开发,Dart是一种强类型、面向对象的编程语言,它既可以编译成JavaScript代码(用于Web应用),也可以编译成机器码(用于移动应用)
- 在移动端,Flutter应用运行在Dart VM(虚拟机)中。Dart VM提供了即时编译(JIT)和预编译(AOT)两种编译方式。在开发过程中,Dart VM采用即时编译,这使得Flutter具有热重载功能,开发者可以在不重新编译整个应用的情况下查看代码更改的效果。在发布应用时,Dart VM会采用预编译,将Dart代码编译成高效的机器码,以提高应用的性能
skia图形库
- Flutter使用Skia图形库进行UI渲染。Skia是一种高性能的2D图形渲染引擎,用于绘制图形和文本。由于Flutter直接使用Skia进行渲染,它无需依赖于原生UI组件,可以实现统一的跨平台UI渲染。这使得Flutter应用具有高度的可定制性,同时还保持了流畅的性能和优美的视觉效果。
跨端容器-通用原理
- UI组件
- 渲染引擎
- 逻辑控制引擎(一般是JS,但Flutter使用dart vm)
- 通信桥梁
- 底层API抹平表现差异
跨端容器-思考??
同样是基于webview渲染,为什么小程序体验比webview流畅
因为小程序做了离线缓存,大部分资源是本地的。而webview要去服务器加载资源,进入页面先有loading页面,会先去加载需要的资源。小程序是针对webview做了一些精致的,对于一些比较危险的操作,比如dom操作,是封死的。
未来的跨端方案会是什么:webview(最广)
总结
感想
这门课程是目前为止觉得最陌生也最有意思的课程。系统讲解了浏览器的架构以及运行原理,结合我之前学的服务器域名查找web程序的知识就可以总结出输入url时浏览器会发生什么过程。输入域名;回车。先检查hosts配置文件里面有没有www.xsn.com域名的映射(一个IP地址可以对应多个域名,一个域名对应一个IP), 有则直接返回对应的IP地址127.0.0.1,这个地址有我们需要访问的web程序,可以直接访问。 没有则去DNS服务器(管理所有域名的地方)找。 对于web程序的访问,浏览器主进程会处理输入,然后开始导航,导航后去浏览器读取响应,再寻找渲染进程去渲染这次加载,拿到html/css/js脚本后,构建dom,cssom,由这两者构成渲染树,之后开始布局,再绘制图层,最后合成,进行展示,展示完成,返回浏览器主进程。(比如输入index.html,导航到该html的位置,读取浏览器响应回来的html、css文件,交由渲染进程渲染页面,包括加载html文件和图片、CSS样式、JS脚本)