性能优化之从URL到页面展示

今天从性能优化的角度再来看看从URL到页面展示,前两篇是从URL到页面展示的流程说起,中间过程发生了什么,并没有突出性能优化点,当然若不知其中间发生了什么不知其原理又谈何性能优化,所以若是还没看过之前的两篇笔记,可以先结合之前两篇一起看。

《从输入URL到页面展示,这中间发生了什么?》
《浏览器页面的渲染流程》

为了整理这一篇文章,特意在掘金上购买了《前端性能优化原理与实践》小册(ps: 要有输入才有输出哈),里面主要是“从输入 URL 到页面加载完成,发生了什么?”作为引子开启话题,这个面试题从大处着手思考,就是两个重要知识维度,一是网络层面,二是渲染层面。往细处说前者牵涉到DNS(域名解析系统)、IP寻址、TCP连接、http请求与响应等;后者则是进程与线程概念、DOM树、层叠样式、重排与重绘及合成等。
小册里面有一张性能优化的思维导图贴出来分享给大家

网络层面性能优化

关于网络层面优化,映入眼帘应该是资源请求与加载,至于DNS域名解析、IP寻址、TCP连接这种网络基础设施我们前端领域也做不了任何优化,而关于资源请求与加载的优化,就有很多方面着手,比如源头上代码打包压缩、构建优化,就要连同webpack工程化去做相关方面优化工作了。在webpack 的优化方面主要体现两个方面:

  • webpack 的构建过程太花时间
  • webpack 打包的结果体积太大

webpack常见的优化方案

  • 利用DllPlugin构建常用的依赖库
  • 使用Happypack将 loader 由单进程转为多进程
  • Tree-Shaking摇树功能在打包时提前去除无用代码
  • 利用缓存加速二次构建速度
  • 按需加载
    核心在于require.ensure(dependencies, callback, chunkName)
  • 利用Gzip压缩
    在我们请求资源头里request headers加上一句:accept-encoding: gzip

相信还有其他的webpack优化方案,欢迎各位补充哦~

图片的优化

我们常说页面优化要从关键资源入手,其实忽略了页面优化关键在于图片的优化。不知道大家认不认同这一观点,可能对于做电商来说非常认同,因为做电商本质上就是做图片;但不管怎样,图片的优化肯定是我们前端领域性能优化重要一环。想想我们工作中的图片优化,是不是在图片大小和质量上做“权衡”,所谓的优化相当于在做“权衡”,牺牲图片质量追求体验和性能。

我们熟悉下图片的几种格式:

  • JPEG/JPG 特点:有损压缩、体积小、加载快、不支持透明
  • PNG-8 与 PNG-24 特点:无损压缩、质量高、体积大、支持透明
  • SVG 特点:文本文件、体积小、不失真、兼容性好
  • Base64 特点:文本文件、依赖编码、小图标解决方案
  • WebP 特点:支持有损压缩和无损压缩,全能型选手

在工作中以上几种格式相信都用过,其实图片的优化还有很多有待挖掘,若不结合工作深入,很难有自己深刻的见解。好比性能优化并不好学,根本原因在于前端技术复杂又日新月异,知识不成体系,难以切入。

缓存优化

接着来说页面的非图片资源加载优化,也就是资源缓存优化,一来减少网络 IO 消耗,二来提高访问速度。浏览器缓存是一种操作简单、效果显著的前端性能优化手段。

浏览器缓存机制

览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:

1、Memory Cache;2、Service Worker Cache ; 3、HTTP Cache ;4、Push Cache

  • MemoryCache
    指的是存在内存中的缓存。从优先级上来说,它是浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。

  • Service Worker
    是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。
    PS:大家注意 Server Worker 对协议是有要求的,必须以 https 协议为前提。

  • HTTP Cache
    是我们日常开发中最为熟悉的一种缓存机制。它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。

强缓存的实现:从 (http1.0) expires 到 (http1.1) cache-control
协商缓存的实现:从 Last-Modified 到 Etag
协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。

关于http缓存下面贴一张权威的流程图:


  • Push Cache
    指的是 HTTP2 在 server push 阶段存在的缓存。因为知识比较新工作上没有用过,不做过多笔记整理。
浏览器本地存储Web Storage

最后到了浏览器的本地存储数据了,在HTML5之前一直是cookie,那是为了存储会话session状态,后面随着技术发展有了localStorage和sessionStorage,以满足丰富的页面数据缓存需要。关于web Storage他们之间的区别在于生命周期作用域

  • 生命周期:Local Storage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 Session Storage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。

  • 作用域:Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 Session Storage 内容便无法共享。

除了耳熟能详的cookie、localStorage、sessionStorage,还有稀疏平常的浏览器数据库IndexDB,因为工作中没有用过,没有太多感触。不过感兴趣的还是可以了解一下,当然也需要有一个感性认识,万一后面数据复杂需要本地存储要用到浏览器数据库呢。
参考阮一峰的网络日志《浏览器数据库 IndexedDB 入门教程》

渲染层面的优化

梳理完了网络层面的优化,紧接着来看看渲染层面的优化部分。先回顾下浏览器渲染进程各个阶段的工作流程图


其中我们重点关注被HTML解释器解析的DOM、被CSS解释器解析的计算属性style、图层布局模块、图层绘制模块、视图合成模块。因为这些地方我们是可以做相关优化的,下面就从这5个方面一一梳理优化的点。


DOM的优化

对于DOM的优化并不陌生,主要在于减少DOM节点的嵌套深度、以及DOM节点的操作,以此避免渲染树的重排与重绘。

  • 重排:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程也叫回流。
  • 重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式,会跳过重排环节。

CSS的优化

关于CSS的优化,主要体现在书写规范上面、CSS资源加载顺序上面、以及CSS动画上面。

CSS的样式规则

首先要知道CSS引擎查找样式表,对每条规则都按从右到左的顺序去匹配。知道这一知识点很重要,当我们在写样式的时候,就要避免使用通配符*,或者是元素标签,尽量使用选择器;另外要合理使用嵌套,不要多层深度嵌套(最高三层嵌套),尽可能使用类来关联每一个标签元素。

CSS的阻塞

根据上面的页面渲染流程图,我们知道CSS的加载阻塞是会影响到页面渲染的,也就是说:

CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

对于CSS的阻塞优化,小册里面总结得很好,做到两点即可:一是尽早,将 CSS 放在 head 标签里;二是尽快,启用 CDN 实现静态资源加载速度的优化。

CSS的动画

CSS的动画方面优化主要是在合成环节上优化,例如使用transform动画会跳过渲染流程中的重排与重绘环节,直接进入合成阶段,因为transform属性是元素的既不布局也不绘制的属性。另外,合成相对于重排重绘来说,会大大提升绘制效率。


JS的优化

关于JS对于页面渲染的优化,主要也是阻塞和对DOM的操作,优化的点也在于减少重排和重绘。

JS的加载方式
  • 正常模式
<script src="index.js"></script>

一般我们会把js文件置于body结束标签的位置,是根据浏览器渲染原理来的,避免阻塞。

  • async 模式
<script async src="index.js"></script>

async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。

  • defer 模式
<script defer src="index.js"></script>

defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。

JS操作DOM优化

在JS中尽量减少 DOM 操作,避免过度渲染。比如:

let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){ 
  // 先对内容进行操作
  content += '<span>我是一个小测试</span>'
} 
// 内容处理好了,最后再触发DOM的更改
container.innerHTML = content

另外可以用DocumentFragment给DOM分压,减少DOM的操作。

let container = document.getElementById('container')
// 创建一个DOM Fragment对象作为容器
let content = document.createDocumentFragment()
for(let count=0;count<10000;count++){
  // span此时可以通过DOM API去创建
  let oSpan = document.createElement("span")
  oSpan.innerHTML = '我是一个小测试'
  // 像操作真实DOM一样操作DOM Fragment对象
  content.appendChild(oSpan)
}
// 内容处理好了,最后再触发真实DOM的更改
container.appendChild(content)
JS中规避重排与重绘
  • 利用变量缓存起来,避免频繁改动
    有时我们想要通过多次计算得到一个元素的布局位置,我们这样做:
// 缓存offsetLeft与offsetTop的值
const el = document.getElementById('el') 
let offLeft = el.offsetLeft, offTop = el.offsetTop
// 在JS层面进行计算
for(let i=0;i<10;i++) {
  offLeft += 10
  offTop  += 10
}
// 一次性将计算结果应用到DOM上
el.style.left = offLeft + "px"
el.style.top = offTop  + "px"
  • 避免逐条改变样式,使用类名去合并样式
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
// 使用类名
const container = document.getElementById('container')
container.classList.add('basic_style')
  • 将 DOM “离线”
    操作DOM可以先display:none,在操作它的属性,完成后再display:block显示出来。对于频繁操作改变它的属性来说也是不错的优化方法。

  • Flush 队列:浏览器并没有那么简单

// 这段代码里,浏览器进行了多少次的回流或重绘呢?
let container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

上面代码片段浏览器会进行4次重排或重绘操作吗?我们自己可以动手试一试,其实并不然哦,因为现代浏览器是很聪明的。浏览器自己也清楚,如果每次 DOM 操作都即时地反馈一次回流或重绘,那么性能上来说是扛不住的。于是它自己缓存了一个 flush 队列,把我们触发的回流与重绘任务都塞进去,待到队列里的任务多起来、或者达到了一定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队。所以上面就算我们进行了 4 次 DOM 更改,也只触发了一次 Layout 和一次 Paint。

小结

关于性能优化,真有一种说不清道不明的感觉。这里只是“从URL到页面展示”的角度来学习性能优化的知识,相信内容有很多是浮于表面,具体业务场景肯定是更加复杂多变,所以说前端的性能优化点是错综复杂,比较综合考验个人的工作能力。Anyway,梳理就到此为止,总之,性能优化是一个路漫漫其修远兮的过程,痛并快乐着做吧~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容