[TOC]
## HTML
### AMP HTML
> AMP是什么
Google 前沿的 AMP 「 Accelerated Mobile Pages 」技术,能使用户从搜索引擎当中进入我们页面的体验得到一个极大的提升。确切地说,AMP并不是一门新的技术,它只是一种能让页面打开得更快的解决方案。
> 我们为什么选择AMP
1、AMP能够带来SEO排名优化。
2、AMP Cache能够让我们充分借助Google CDN Cache的优势。虽然我们内部已经做了很多优化,包括DNS预热,但如果能有Google全球CDN支持就更是件好事。
3、Google搜索结果对AMP页面有预加载处理,能让用户更快地到达我们的着陆页。
~~~
<!doctype html>
<html ⚡>
<head>
<meta charset="utf-8">
<link rel="canonical" href="hello-world.html">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<style amp-boilerplate>
body{
-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;
-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;
-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;
animation:-amp-start 8s steps(1,end) 0s 1 normal both}
@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
</style>
<noscript>
<style amp-boilerplate>
body{
-webkit-animation:none;
-moz-animation:none;
-ms-animation:none;
animation:none
}
</style>
</noscript>
<script async src="https://cdn.ampproject.org/v0.js"></script>
</head>
<body>Hello World!</body>
</html>
~~~
<br>
### 减少没有必要的嵌套
~~~
<div class="form">
<form>
...
</form>
</div>
// 可以改写为
<form class="form">
...
</form>
~~~
<br>
### 减少使用iframe
用iframe可以把一个HTML文档插入到父文档里,重要的是明白iframe是如何工作的并高效地使用它。
iframe的优点:
* 引入缓慢的第三方内容,比如标志和广告
* 安全沙箱
* 并行下载脚本
iframe的缺点:
* 代价高昂,即使是空白的iframe
* 阻塞页面加载
* 非语义
<br>
### DNS Prefetching
DNS Prefetch是一种DNS预解析技术,当我们浏览网页时,浏览器会在加载网页时对网页中的域名进行预解析并缓存,这样在浏览器加载网页中的链接时,就无需进行DNS解析,减少用户的等待时间,提高用户体验。DNS Prefetch现已被主流浏览器支持,大多数浏览器针对DNS解析都进行了优化,典型的一次DNS解析会耗费20~120ms,减少DNS解析时间和次数是个很好的优化措施。这里附上一张Can I use it官网上的DNS Prefetch支持情况图:
![](https://box.kancloud.cn/d3c711392d52895649d66b8f3b4b8047_720x357.png)
用法
~~~
<link rel="dns-prefetch" href="//tj.koudaitong.com/" />
<link rel="dns-prefetch" href="//imgqn.koudaitong.com/" />
<link rel="dns-prefetch" href="//kdt-static.qiniudn.com/" />
// 强制开启DNS Prefetching
<meta http-equiv="x-dns-prefetch-control" content="on">
~~~
结论
* 对于引用了大量很多其他域名的资源的网页会有作用,如果你的网站,基本所有的资源都在你本域名下,那么这个基本没有什么作用。因为DNS Chrome在访问你的网站就帮你缓存了。
* 一般情况下所有的a标签的href都会自动去启用DNS Prefetching,网页的a标签href带的域名,是不需要在head里面加上link手动设置的
* a标签的href是可以在chrome、firefox包括高版本的IE,但是在HTTPS下面不起作用,需要meta来强制开启功能
* 对重定向跳转的新域名做手动dns prefetching,比如:页面上有个A域名的链接,但访问A会重定向到B域名的链接,这么在当前页对B域名做手动dns prefetching是有意义的
功能的有效性
* 如果本地就有缓存,那么解析大概是0~1ms,如果去路由器查找大概是15ms,如果当地的服务器,一些常见的域名可能需要150ms左右,那么不常见的可能要1S以上。
* DNS解析的包很小,因为DNS是分层协议的,不需要跟http协议一样,一个UDP的包就ok,大概100bytes,快速。
* 本机的DNS缓存是有限,例如XP大概50到200个域名,所以Chrome这里做了优化,会根据你的网站访问频率,来保证你常用的网站的DNS都能被缓存住。
在chrome的地址栏输入查看
~~~
"about:histograms/DNS"
"about:dns"
~~~
<br>
### preconnet
浏览器要建立一个连接,一般需要经过DNS查找,TCP三次握手和TLS协商(如果是https的话),这些过程都是需要相当的耗时的,所以preconnet,就是一项使浏览器能够预先建立一个连接,等真正需要加载资源的时候就能够直接请求了。
而一般形式就是
~~~
<link rel="preconnect" href="//example.com">
<link rel="preconnect" href="//cdn.example.com" crossorigin>
~~~
浏览器会进行以下步骤:
* 解释href的属性值,如果是合法的URL,然后继续判断URL的协议是否是http或者https否则就结束处理
* 如果当前页面host不同于href属性中的host,crossorigin其实被设置为anonymous(就是不带cookie了),如果希望带上cookie等信息可以加上crossorign属性,corssorign就等同于设置为use-credentials
<br>
### prefetch
能够让浏览器预加载一个资源(HTML,JS,CSS或者图片等),可以让用户跳转到其他页面时,响应速度更快。
一般形式就是
~~~
<link rel="prefetch" href="//example.com/next-page.html" as="html" crossorigin="use-credentials">
<link rel="prefetch" href="/library.js" as="script">
~~~
虽然是预加载了,但是页面是不会解析或者JS是不会直接执行的。
<br>
### prerender
而prerender不仅会加载资源,还会解执行页面,进行预渲染,但是这都是根据浏览器自身进行判断。
浏览器可能会分配少量资源对页面进行预渲染挂起部分请求直至页面可见时可能会放弃预渲染,如果消耗资源过多等等情况。。。
一般形式
~~~
<link rel="prerender" href="//example.com/next-page.html">
~~~
<br>
## CSS
### 把样式表放在顶部
通常情况下 CSS 被认为是阻塞渲染的资源,在CSSOM 构建完成之前,页面不会被渲染,放在顶部让样式表能够尽早开始加载。但如果把引入样式表的 link 放在文档底部,页面虽然能立刻呈现出来,但是页面加载出来的时候会是没有样式的,是混乱的。当后来样式表加载进来后,页面会立即进行重绘,这也就是通常所说的闪烁了。
<br>
### 内联首屏关键CSS(Critical CSS)
性能优化中有一个重要的指标——首次有效绘制(First Meaningful Paint,简称FMP)即指页面的首要内容(primary content)出现在屏幕上的时间。这一指标影响用户看到页面前所需等待的时间,而 **内联首屏关键CSS**能减少这一时间。
<br>
既然内联CSS能够使页面渲染的开始时间提前,那么是否可以内联所有的CSS呢?答案显然是否定的,这种方式并不适用于内联较大的CSS文件。因为[初始拥塞窗口](https://link.juejin.im?target=https%3A%2F%2Ftylercipriani.com%2Fblog%2F2016%2F09%2F25%2Fthe-14kb-in-the-tcp-initial-window%2F)存在限制(TCP相关概念,通常是 14.6kB,压缩后大小),如果内联CSS后的文件超出了这一限制,系统就需要在服务器和浏览器之间进行更多次的往返,这样并不能提前页面渲染时间。因此,我们应当**只将渲染首屏内容所需的关键CSS内联到HTML中**。
<br>
既然已经知道内联首屏关键CSS能够优化性能了,那下一步就是如何确定首屏关键CSS了。显然,我们不需要手动确定哪些内容是首屏关键CSS。Github上有一个项目Critical CSS4,可以将属于首屏的关键样式提取出来,大家可以看一下该项目,结合自己的构建工具进行使用。当然为了保证正确,大家最好再亲自确认下提取出的内容是否有缺失。
<br>
不过内联CSS有一个缺点,内联之后的CSS不会进行缓存,每次都会重新下载。不过如上所说,如果我们将内联后的文件大小控制在了14.6kb以内,这似乎并不是什么大问题。
<br>
### 异步加载CSS
CSS会阻塞渲染,在CSS文件请求、下载、解析完成之前,浏览器将不会渲染任何已处理的内容。有时,这种阻塞是必须的,因为我们并不希望在所需的CSS加载之前,浏览器就开始渲染页面。那么将首屏关键CSS内联后,剩余的CSS内容的阻塞渲染就不是必需的了,可以使用外部CSS,并且异步加载。
#### JavaScript动态创建
~~~
// 创建link标签
const myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到header的最后位置
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );
~~~
<br>
#### 将link元素的 media 属性设置为用户浏览器不匹配的媒体类型
将link元素的`media`属性设置为用户浏览器不匹配的媒体类型(或媒体查询),如`media="print"`,甚至可以是完全不存在的类型`media="noexist"`。对浏览器来说,如果样式表不适用于当前媒体类型,其优先级会被放低,会在不阻塞页面渲染的情况下再进行下载。
当然,这么做只是为了实现CSS的异步加载,别忘了在文件加载完成之后,将`media`的值设为`screen`或`all`,从而让浏览器开始解析CSS。
~~~
<link rel="stylesheet" href="mystyles.css" media="noexist" onload="this.media='all'">
~~~
<br>
#### 将 link 元素标记为 alternate
通过`rel`属性将`link`元素标记为`alternate`可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将`rel`改回去。
~~~
<link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='stylesheet'">
~~~
<br>
#### rel="preload"
上述的三种方法都较为古老。现在,[rel="preload"](https://link.juejin.im?target=https%3A%2F%2Fwww.w3.org%2FTR%2Fpreload%2F)这一Web标准指出了如何异步加载资源,包括CSS类资源。
~~~
<link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'">
~~~
注意,`as`是必须的。忽略`as`属性,或者错误的`as`属性会使`preload`等同于`XHR`请求,浏览器不知道加载的是什么内容,因此此类资源加载优先级会非常低。`as`的可选值可以参考上述标准文档。
<br>
看起来,`rel="preload"`的用法和上面两种没什么区别,都是通过更改某些属性,使得浏览器异步加载CSS文件但不解析,直到加载完成并将修改还原,然后开始解析。
<br>
但是它们之间其实有一个很重要的不同点,那就是**使用preload,比使用不匹配的`media`方法能够更早地开始加载CSS**。所以尽管这一标准的支持度还不完善,仍建议优先使用该方法。
<br>
### 避免使用CSS表达式
<br>
### 减少选择器层级
~~~
.wrapper .list .item .success {}
// 可以写成如下:
.wrapper .list .item-success {}
~~~
<br>
### 关键选择器要尽量特殊
CSS 选择器在匹配的时候是由右至左进行的,因此最后一个选择器常被称为关键选择器,因为最后一个选择越特殊,需要进行匹配的次数越少。要千万避免使用 *(通用选择器)作为关键选择器。因为它能匹配到所有元素,进而倒数第二个选择器还会和所有元素进行一次匹配。这导致效率很低下。
另外 first-child 这类伪类选择器也不够特殊,也要避免将它们作为关键选择器。关键选择器越特殊,浏览器就能用较少的匹配次数找到待匹配元素,选择器性能也就越好。
<br>
### 选择<link>舍弃@import
前面提到了一个最佳实践:为了实现逐步渲染,CSS应该放在顶部。
在IE中用@import与在底部用<link>效果一样,所以最好不要用它。
<br>
### 避免使用滤镜
IE专有的AlphaImageLoader滤镜可以用来修复IE7之前的版本中半透明PNG图片的问题。在图片加载过程中,这个滤镜会阻塞渲染,卡住浏览器,还会增加内存消耗而且是被应用到每个元素的,而不是每个图片,所以会存在一大堆问题。
最好的方法是干脆不要用AlphaImageLoader,而优雅地降级到用在IE中支持性很好的PNG8图片来代替。如果非要用AlphaImageLoader,应该用下划线hack:_filter来避免影响IE7及更高版本的用户。
<br>
### 将渐变或者会动画元素放到单独的绘制层中
绘制并非在一个单独的画布上进行的,而是多层。因此将那些会变动的元素提升至单独的图层,可以让他的改变影响到的元素更少。
可以使用 CSS 中的 will-change: transform; 或者 transform: translateZ(0); 这样来将元素提升至单独的图层中。
在调试的时候你可以在 Chrome DevTools 的 timeline 面板来观察绘制图层。当然也不是说图层越多越好,因为新增加一个图层可能会耗费额外的内存。且新增加一个图层的目的是为了避免某个元素的变动影响其他元素。
<br>
### 使用flexbox替代老的布局模型
老的布局模型以相对/绝对/浮动的方式将元素定位到屏幕上
Floxbox布局模型用流式布局的方式将元素定位到屏幕上
通过一个小实验可以看出两种布局模型的性能差距,同样对1300个元素布局,浮动布局耗时14.3ms,Flexbox布局耗时3.5ms。
![](https://box.kancloud.cn/03780916a4bb3d96f9409bc2f4d373b0_2294x768.png)
![](https://box.kancloud.cn/cea4859a9407a88986e22f3d1f393384_1044x899.png)
<br>
### 利用GPU硬件加速浏览器渲染
应用动画效果的元素应该被提升到其自有的渲染层,但不要滥用。
在页面中创建一个新的渲染层最好的方式就是使用CSS属性winll-change,对于目前还不支持will-change属性、但支持创建渲染层的浏览器,可以通过3D transform属性来强制浏览器创建一个新的渲染层。需要注意的是,不要创建过多的渲染层,这意味着新的内存分配和更复杂的层管理。
对于我们的浏览器而言,拿到我们的html文本串开始按顺序解析成DOM树,并与同步解析出来的CSS匹配生成渲染树(跟DOM树的节点不是一一对应,比如display:none的节点就不会插入渲染树)
![](https://box.kancloud.cn/dfc7f944807cf729902e986820a215d1_554x257.png)
浏览器将渲染树的节点用一个图层表示,这样层层叠加在一起生成layout,有点像ps的图层叠加的概念(可以通过火狐浏览器开发者工具3维展示更直观),一般情况下对节点的任何涉及尺寸的改变都会引起layout的重排重绘(重排和重绘是造成浏览器渲染的最大性能损耗的因素),但有种开小灶的情况Composite Layers(复合图层)直接交给我们GPU中单独的合成器进程处理,自身变化不会引起其他层的位置变化,不会引起重排重绘。tranform 3d 或 winll-change悄悄的告诉我们的浏览器把元素解析作为复合图层交给单独进程去处理的。
~~~
.moving-elemen {
will-change: transform;
transform: translateZ(0);
}
~~~
![](https://box.kancloud.cn/7631de4f79c019b57b149711b99c8015_987x614.png)
![](https://box.kancloud.cn/9c04ebba4b4165601797a471182c1d77_988x643.png)
<br>
#### 用transform/opacity实现动画效果
使用transform/opacity实现动画效果,会跳过渲染流程的布局和绘制环节,只做渲染层的合并。
![](https://box.kancloud.cn/587337e663bf5d61a718d9523f6a14aa_698x334.png)
使用transform/opacity的元素必须独占一个渲染层,所以必须提升该元素到单独的渲染层。
<br>
### 减少回流和重绘
当页面布局和几何属性改变时就需要回流。下述情况会发生浏览器回流:
* 添加或者删除可见的DOM元素;
* 元素位置改变;
* 元素尺寸改变(margin、padding、border、width和height)
* 内容改变,尤其是输入控件
* 页面渲染初始化;
* 浏览器窗口尺寸改变(resize事件发生时);
* 改变文字大小;
* 激活伪类,如:hover;
* offsetWidth, width, clientWidth, scrollTop/scrollHeight的计算, 会使浏览器将渐进回流队列Flush,立即执行回流;
* 设置style属性;
<br>
减少回流、重绘其实就是需要减少对render tree的操作(合并多次多DOM和样式的修改),并减少对一些style信息的请求,尽量利用好浏览器的优化策略。具体方法有:
#### 直接改变className
如果动态改变样式,则使用cssText(考虑没有优化的浏览器)
~~~
// 不好的写法
var changeDiv = document.getElementById('changeDiv');
changeDiv.style.color = '#093';
changeDiv.style.background = '#eee';
changeDiv.style.height = '200px';
// 比较好的写法
div.changeDiv {
background: #eee;
color: #093;
height: 200px;
}
document.getElementById('changeDiv').className = 'changeDiv';
~~~
#### 让要操作的元素进行”离线处理”,处理完后一起更新
* 使用DocumentFragment进行缓存操作,引发一次回流和重绘;
* 使用display:none技术(由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发2次重排。);
* 使用cloneNode(true or false) 和 replaceChild 技术,引发一次回流和重绘;
* 将需要多次重排的元素,position属性设为absolute或fixed;
不要经常访问会引起浏览器flush队列的属性,如果你确实要访问,利用缓存
~~~
// 不好的写法
for(循环) {
el.style.left = el.offsetLeft + 5 + "px";
el.style.top = el.offsetTop + 5 + "px";
}
// 比较好的写法
var left = el.offsetLeft,
top = el.offsetTop,
s = el.style;
for (循环) {
left += 10;
top += 10;
s.left = left + "px";
s.top = top + "px";
}
~~~
#### 让元素脱离动画流
具有复杂动画的元素绝对定位-脱离文档流,避免强烈的回流。现代浏览器可以渐进使用CSS3 transition实现动画效果,比改变像素值来的高性能。
####在内存中多次操作节点,完成后再添加到文档中
例如要异步获取表格数据,渲染到页面。可以先取得数据后在内存中构建整个表格的html片段,再一次性添加到文档中去,而不是循环添加每一行。
#### 不要用tables布局
tables中某个元素一旦触发reflow就会导致table里所有的其它元素reflow。在适合用table的场合,可以设置table-layout为auto或fixed,这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
#### 适当定高
例如如果div内容可能有高度差异的动态内容载入。例如右上角的个人用户信息是页面渲染完毕之后动态载入的。但是,有可能会出现高度20像素的小图标,用户信息,而文字所占据高度为12px * 1.4 = 16.8px, IE6又存在行高被拒的悲剧。因此,如果这部分div不定高,就会出现个人信息载入后,整个页面下沉几像素(3.2像素?)页面重绘的问题。
#### 图片设定不响应重绘的尺寸
如果你的img标签不设定尺寸、同时外部容器没有定死高宽,则图片在首次载入时候,占据空间会从0到完全出现,左右上下都可能位移,发生大规模的重绘。可以参见新浪微博载入时候页面高度随着图片显示不断变高的问题,这些都让浏览器重绘了,一是体验可能不好,二是烧CPU的。
你可以使用width/height控制,或者在CSS中设置。
<br>
## JavaScript
### 懒执行
懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒。
### 尽量减少DOM访问
用JavaScript访问DOM元素是很慢的,所以,为了让页面反应更迅速,应该:
* 缓存已访问过的元素的索引
* 先“离线”更新节点,再把它们添到DOM树上
* 避免用JavaScript修复布局问题
<br>
### 批量操作 DOM
在必须要进行频繁的 DOM 操作时,可以使用 fastdom 这样的工具,它的思路是将对页面的读取和改写放进队列,在页面重绘的时候批量执行,先进行读取后改写。因为如果将读取与改写交织在一起可能引起多次页面的重排。而利用 fastdom 就可以避免这样的情况发生。
虽然有了 fastdom 这样的工具,但有的时候还是不能从根本上解决问题,比如我最近遇到的一个情况,与页面简单的一次交互(轻轻滚动页面)就执行了几千次 DOM 操作,这个时候核心要解决的是减少 DOM 操作的次数。这个时候就要从代码层面考虑,看看是否有不必要的读取。
<br>
### 利用事件冒泡特性
浏览器事件注册有3个级别定义,DOM 0级事件注册(利用DOM元素行内事件属性onclick注册事件回调),DOM 1级事件注册(利用DOM元素对象的onclick API 在外部注册事件回调),DOM 2级事件注册(利用DOM元素对象的addEventListner/attachEvent API 在外部注册事件回调)。这里性能优化的建议就是利用DOM2级在目标DOM的父标签(大部分框架是在body标签统一注册事件监听)注册回调,收拢事件监听入口同时节约了DOM节点引用开销。
<br>
### 把JavaScript和CSS放到外面
很多性能原则都是关于如何管理外部组件的,然而,在这些顾虑出现之前你应该问一个更基础的问题:应该把JavaScript和CSS放到外部文件中还是直接写在页面里?
实际上,用外部文件可以让页面更快,因为JavaScript和CSS文件会被缓存在浏览器。HTML文档中的行内JavaScript和CSS在每次请求该HTML文档的时候都会重新下载。这样做减少了所需的HTTP请求数,但增加了HTML文档的大小。另一方面,如果JavaScript和CSS在外部文件中,并且已经被浏览器缓存起来了,那么我们就成功地把HTML文档变小了,而且还没有增加HTTP请求数。
<br>
### 对高频触发的事件进行节流或消抖
debounce 和 throttle 是两个相似(但不相同)的用于控制函数在某段事件内的执行频率的技术。你可以在 underscore 或者 lodash 中找到这两个函数。
使用 debounce 进行消抖
多次连续的调用,最终实际上只会调用一次。想象自己在电梯里面,门将要关上,这个时候另外一个人来了,取消了关门的操作,过了一会儿门又要关上,又来了一个人,再次取消了关门的操作。电梯会一直延迟关门的操作,直到某段时间里没人再来。
所以 debounce 适合用在比如对用户输入内容进行校验的这种场景下,多次触发只需要响应最后一次触发就好了。
使用 throttle 进行节流
将频繁调用的函数限定在一个给定的调用频率内。它保证某个函数频率再高,也只能在给定的事件内调用一次。比如在滚动的时候要检查当前滚动的位置,来显示或隐藏回到顶部按钮,这个时候可以使用 throttle 来将滚动回调函数限定在每 300ms 执行一次。
这两个函数都接受一个函数作为参数,然后返回一个节流/去抖后的函数:
~~~
// 错误的用法,每次事件触发都得到一个新的函数
$(window).on('scroll', function() {
_.throttle(doSomething, 300);
});
// 正确的用法,将节流后的函数作为回调
$(window).on('scroll', _.throttle(doSomething, 200));
~~~
<br>
### 计算缓存
<br>
### 网络IO缓存
<br>
### 使用 requestAnimationFrame 来更新页面
`setTimeout(callback)` 和 `setInterval(callback)` 无法保证 callback 函数的执行时机,很可能在帧结束的时候执行,从而导致丢帧,如下图:
![](https://box.kancloud.cn/624838d05b96d36bffb3553532e65119_762x334.png)
`requestAnimationFrame(callback)` 可以保证 callback 函数在每帧动画开始的时候执行。
~~~
// requestAnimationFrame将保证updateScreen函数在每帧的开始运行
requestAnimationFrame(updateScreen);
~~~
注意:jQuery的 `animate` 函数就是用 `setTimeout` 来实现动画,可以通过[jquery-requestAnimationFrame](https://link.jianshu.com/?t=https://github.com/gnarf/jquery-requestAnimationFrame)这个补丁来用 `requestAnimationFrame` 替代 `setTimeout`
![](https://box.kancloud.cn/af25f6069bd1bf942f199e7ef9e70a20_987x640.png)
<br>
### 数据结构,算法优化
数据结构和算法的优化是前端接触比较少的。但是如果碰到计算量比较大的运算,除了运用缓存之外,还要借助一定的数据结构优化和算法优化。
比如现在有50,000条订单数据。
~~~
const orders = [{name: 'john', price: 20}, {name: 'john', price: 10}, ....]
~~~
我需要频繁地查找其中某个人某天的订单信息。 我们可以采取如下的数据结构:
~~~
const mapper = {
'john|2015-09-12': []
}
~~~
这样我们查找某个人某天的订单信息速度就会变成O(1),也就是常数时间。你可以理解为索引,因为索引是一种数据结构,那么我们也可以使用其他数据结构和算法适用我们各自独特的项目。对于算法优化,首先就要求我们能够识别复杂度,常见的复杂度有O(n) O(logn) O(nlogn) O(n2)。而对于前端,最基本的要识别糟糕的复杂度的代码,比如n三次方或者n阶乘的代码。虽然我们不需要写出性能非常好的代码,但是也尽量不要写一些复杂度很高的代码。
<br>
### 多线程计算
通过HTML5的新API webworker,使得开发者可以将计算转交给worker进程,然后通过进程通信将计算结果回传给主进程。毫无疑问,这种方法对于需要大量计算有着非常明显的优势。
~~~
var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);
// The main thread is now free to continue working on other things...
dataSortWorker.addEventListener('message', function(evt) {
var sortedData = evt.data;
// Update data on screen...
});
~~~
由于WebWorker 被做了很多限制,使得它不能访问诸如window,document这样的对象,因此如果你需要使用的话,就不得不寻找别的方法。
一种使用web worker的思路就是分而治之,将大任务切分为若干个小任务,然后将计算结果汇总,我们通常会借助数组这种数据结构来完成,下面是一个例子:
~~~
// 很多小任务组成的数组
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
// 使用更新的api requestAnimationFrame而不是setTimeout可以提高性能
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
var taskFinishTime;
do {
// Assume the next task is pushed onto a stack.
var nextTask = taskList.pop();
// Process nextTask.
processTask(nextTask);
// Go again if there’s enough time to do the next task.
taskFinishTime = window.performance.now();
} while (taskFinishTime - taskStartTime < 3);
if (taskList.length > 0)
requestAnimationFrame(processTaskList);
}
~~~
> 线程安全问题都是由全局变量及静态变量引起的。 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,就需要考虑线程同步,就可能产生线程安全问题。
<br>
### 延迟加载
html5中给script标签引入了async和defer属性。
带有async属性的script标签,会在浏览器解析时立即下载脚本同时不阻塞后续的document渲染和script加载等事件,从而实现脚本的异步加载。
带有defer属性的script标签,和async拥有类似的功能。并且他们有可以附带一个onload事件`<script src="" defer onload="init()">`。
async和defer的区别在于:async属性会在脚本下载完成后无序立即执行,defer属性会在脚本下载完成后按照document结构顺序执行。
由于defer和async的兼容性问题,我们通常使用动态创建script标签的方式来实现异步加载脚本,即
~~~
document.write(' < script src="" async></script>');
~~~
,该方式也可以避免阻塞。
ga统计代码采用就是动态创建script标签方案。
该方法不阻塞页面渲染,不阻塞后续请求,但会阻塞window.onload事件,页面的表现方式是进度条一直加载或loading菊花一直转。
所以我们延迟执行ga初始化代码,将其放到window.onload函数中去执行,可以防止ga脚本阻塞window.onload事件。从而让用户感受到更快的加载速度。
![](https://box.kancloud.cn/67041c13b02099ca6da5a0779384c623_576x154.png)
### PWA(Progressive Web Apps)方案
<br>
## 图片
### 懒加载
图片延迟加载的原理就是先不设置img的src属性,等合适的时机(比如滚动、滑动、出现在视窗内等)再把图片真实url放到img的src属性上。
**固定宽高值的图片**
固定宽高值的图片延迟加载比较简单,因为宽高值都可以设置在css中,只需考虑src的替换问题,推荐使用lazysizes。
~~~
// 引入js文件
<script src="lazysizes.min.js" async=""></script>
// 非响应式 例子
<img src="" data-src="image.jpg" class="lazyload" />
// 响应式 例子,自动计算合适的图片
<img
data-sizes="auto"
data-src="image2.jpg"
data-srcset="image1.jpg 300w,
image2.jpg 600w,
image3.jpg 900w" class="lazyload" />
// iframe 例子
<iframe frameborder="0"
class="lazyload"
allowfullscreen=""
data-src="//www.youtube.com/embed/ZfV-aYdU4uE">
</iframe>
~~~
lazysizes延迟加载过程中会改变图片的class:默认lazyload,加载中lazyloading,加载结束:lazyloaded。结合这个特性我们有两种解决上述问题办法:
1、设置opacity:0,然后在显示的时候设置opacity:1。
~~~
// 渐现 lazyload
.lazyload,
.lazyloading{
opacity: 0;
}
.lazyloaded{
opacity: 1;
transition: opacity 500ms; //加上transition就可以实现渐现的效果
}
~~~
2、用一张默认的图占位,比如1x1的透明图或者灰图。
~~~
<img class="lazyload"
src="data:image/gif;base64,R0lGODlhAQABAAA
AACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
data-src="真实url"
alt="<%= article.title %>">
~~~
此外,为了让效果更佳,尤其是文章详情页中的大图,我们可以加上loading效果。
~~~
.article-detail-bd {
.lazyload {
opacity: 0;
}
.lazyloading {
opacity: 1;
background: #f7f7f7 url(/images/loading.gif) no-repeat center;
}
}
~~~
<br>
**固定宽高比的图片**
固定宽高比的图片延迟加载相对来说复杂很多,比如文章详情页的图片,由于设备的宽度值不确定,所以高度值也不确定。
固定宽高比的图片抖动问题,有下列两种主流的方式可以解决:
1、第一种方案使用padding-top或者padding-bottom来实现固定宽高比。优点是纯CSS方案,缺点是HTML冗余,并且对输出到第三方不友好。
~~~
<div style="padding-top:75%">
<img data-src="" alt="" class="lazyload">
<div>
~~~
2、第二种方案在页面初始化阶段利用ratio设置实际宽高值,优点是html干净,对输出到第三方友好,缺点是依赖js,理论上会至少抖动一次。
~~~
<img data-src="" alt="" class="lazyload" data-ratio="0.75">
~~~
那么,这个 `padding-top: 75%` 和 `data-ratio="0.75"` 的数据从哪儿来呢?在你上传图片的时候,需要后台给你返回原始宽高值,计算得到宽高比,然后保存到data-ratio上。
定义了一个设置图片高度的函数:
~~~
// 重置图片高度,仅限文章详情页
function resetImgHeight(els, placeholder) {
var ratio = 0,
i, len, width;
for (i = 0, len = els.length; i < len; i++) {
els[i].src = placeholder;
width = els[i].clientWidth; //一定要使用clientWidth
if (els[i].attributes['data-ratio']) {
ratio = els[i].attributes['data-ratio'].value || 0;
ratio = parseFloat(ratio);
}
if (ratio) {
els[i].style.height = (width * ratio) + 'px';
}
}
}
~~~
我们将以上代码的定义和调用都直接放到了HTML中,就为了一个目的,第一时间计算图片的高度值,降低用户感知到页面抖动的可能性,保证最佳效果。
~~~
// 原生代码
<img alt=""
data-ratio="0.562500"
data-format="jpeg"
class="lazyload"
data-src="http://img.qdaily.com/uploads/20160807124000WFJNyGam85slTC4H.jpg"
src="">
// 解析之后的代码
<img alt=""
data-ratio="0.562500"
data-format="jpeg"
class="lazyloaded"
data-src="http://img.qdaily.com/uploads/20160807124000WFJNyGam85slTC4H.jpg"
src="http://img.qdaily.com/uploads/20160807124000WFJNyGam85slTC4H.jpg"
style="height: 323.438px;">
~~~
我们不仅保存了宽高比,还保存了图片格式,是为了后期可以对gif做进一步的优化。
注意事项
* 避免图片过早加载,把临界值调低一点。在实际项目中,并不需要过早就把图片请求过来,尤其是Mobile项目,过早请求不仅浪费流量,也会因为请求太多,导致页面加载速度变慢。
* 为了最好的防抖效果,设置图片高度的JS代码内嵌到HTML中以便第一时间执行。
* 根据图片宽度设置高度时,使用clientWidth而不是width。这是因为Safari中,第一时间执行的JS代码获取图片的width失败,所以使用clientWidth解决这个问题。
<br>
### Low Quality Image Placeholders
原理见:[Progressive Image Loading using Intersection Observer and SQIP](https://calendar.perfplanet.com/2017/progressive-image-loading-using-intersection-observer-and-sqip/)
步骤:
* 页面初始化时,img元素初始化时,src使用低质量的jpg或svg,显示出图片的大概轮廓
* 页面滚动到当前图片位置,后台启动加载原图
* 原图加载完成,替换掉之前的src显示出原图
可参考知乎
![](https://box.kancloud.cn/f36b1e8d0fd43d46b565a90bb3dc7fcd_1267x157.png)
![](https://box.kancloud.cn/f93fb5bd6562b7e363f4ca9184c1657a_1999x441.png)
![](https://box.kancloud.cn/43fd2941db0398def5251010f2293b15_1728x293.png)
<br>
### 压缩图片体积
首先来看下图片体积的决定因素。这里可能需要一些图像学的相关知识。图片分为位图和矢量图。位图是用比特位来表示像素,然后由像素组成图片。位图有一个概念是位深,是指存储每个像素所用的位数。那么对于位图计算大小有一个公式就是图片像素数 * 位深 bits。 注意单位是bits,也可以换算成方便查看的kb或者mb。
> 图片像素数 = 图片水平像素数 * 图片垂直像素数
而矢量图由数学向量组成,文件容量较小,在进行放大、缩小或旋转等操作时图象不会失真,缺点是不易制作色彩变化太多的图象。那么矢量图是电脑经过数据计算得到的,因此占据空间小。通常矢量图和位图也会相互转化,比如矢量图要打印就会点阵化成位图。
下面讲的图片优化指的是位图。知道了图片大小的决定因素,那么减少图片大小的方式就是减少分辨率或者采用位深较低的图片格式。
<br>
### 减少分辨率
我们平时开发的时候,设计师会给我们1x2x3x的图片,这些图片的像素数是不同的。2x的像素数是1x的 2x2=4倍,而3x的像素数高达3x3=9倍。图片直接大了9倍。因此前端使用图片的时候最好不要直接使用3倍图,然后在不同设备上平铺,这种做法会需要依赖浏览器对其进行重新缩放(这还会占用额外的 CPU 资源)并以较低分辨率显示,从而降低性能。
<br>
### 减少位深
位深是用来表示一个颜色的字节数。位深是24位,表达的是使用256(2的24/3次方)位表示一个颜色。因此位深越深,图片越精细。如果可能的话,减少位深可以减少体积。
<br>
### 压缩
前面说了图片大小 = 图片像素数 * 位深, 其实更严格的是图片大小 = 图片像素数 * 位深 * 图片质量, 因此图片质量(q)越低,图片会越小。 影响图片压缩质量的因素有很多,比如图片的颜色种类数量,相邻像素颜色相同的个数等等。对应着有很多的图片压缩算法,目前比较流行的图片压缩是webp格式。因此条件允许的话,尽量使用webp格式。
<br>
### 裁剪
我们希望可以通过 https://test.imgix.net/some_file?w=395&h=96&crop=faces 的方式指定图片的大小,从而减少传输字节的浪费。已经有图片服务商提供了这样的功能。比如imgix。
> imgix有一个优势就是能够找到图片中有趣的区域并做裁剪。而不是仅仅裁剪出图片的中心
>
上面提到的webp最好也可以通过CDN厂商支持,即我们上传图片的时候,CDN厂商对应存储一份webp的。比如我们上传一个png图片https://img.alicdn.com/test/TB1XFdma5qAXuNjy1XdXXaYcVXa-29-32.png 。然后我们可以通过https://img.alicdn.com/test/TB1XFdma5qAXuNjy1XdXXaYcVXa-29-32.webp 访问其对应的webp资源。我们就可以根据浏览器的支持情况加载webp或者png图片了。
<br>
## 参考资料
[漫谈前端性能优化](https://juejin.im/post/5a4f09eef265da3e3b7a5399#heading-20)
[2018 前端性能优化清单 - 掘金](https://juejin.im/post/5a966bd16fb9a0635172a50a#heading-5)
[AMP项目实战分享](https://zhuanlan.zhihu.com/p/34751588)
[雅虎前端优化的35条军规](http://www.cnblogs.com/xianyulaodi/p/5755079.html#_label0)
[前端性能优化](http://mp.weixin.qq.com/s/qglFD2nHFqFBivb8T23Qtg)
[梳理:提高前端性能方面的处理以及不足 « 张鑫旭-鑫空间-鑫生活](https://www.zhangxinxu.com/wordpress/2013/04/%E5%89%8D%E7%AB%AF%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E7%BB%8F%E9%AA%8C%E5%88%86%E4%BA%AB/)
[完整攻略!让你的网页加载时间降低到 1s 内! - 简书](https://www.jianshu.com/p/d857c3ff78d6)
[CSS性能优化的8个技巧](https://juejin.im/post/5b6133a351882519d346853)
- 第一部分 HTML
- meta
- meta标签
- HTML5
- 2.1 语义
- 2.2 通信
- 2.3 离线&存储
- 2.4 多媒体
- 2.5 3D,图像&效果
- 2.6 性能&集成
- 2.7 设备访问
- SEO
- Canvas
- 压缩图片
- 制作圆角矩形
- 全局属性
- 第二部分 CSS
- CSS原理
- 层叠上下文(stacking context)
- 外边距合并
- 块状格式化上下文(BFC)
- 盒模型
- important
- 样式继承
- 层叠
- 属性值处理流程
- 分辨率
- 视口
- CSS API
- grid(未完成)
- flex
- 选择器
- 3D
- Matrix
- AT规则
- line-height 和 vertical-align
- CSS技术
- 居中
- 响应式布局
- 兼容性
- 移动端适配方案
- CSS应用
- CSS Modules(未完成)
- 分层
- 面向对象CSS(未完成)
- 布局
- 三列布局
- 单列等宽,其他多列自适应均匀
- 多列等高
- 圣杯布局
- 双飞翼布局
- 瀑布流
- 1px问题
- 适配iPhoneX
- 横屏适配
- 图片模糊问题
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 内存空间
- 作用域
- 执行上下文栈
- 变量对象
- 作用域链
- this
- 类型转换
- 闭包(未完成)
- 原型、面向对象
- class和extend
- 继承
- new
- DOM
- Event Loop
- 垃圾回收机制
- 内存泄漏
- 数值存储
- 连等赋值
- 基本类型
- 堆栈溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍历对象属性
- 宽度、高度
- performance
- 位运算
- tostring( ) 与 valueOf( )方法
- JavaScript技术
- 错误
- 异常处理
- 存储
- Cookie与Session
- ES6(未完成)
- Babel转码
- let和const命令
- 变量的解构赋值
- 字符串的扩展
- 正则的扩展
- 数值的扩展
- 数组的扩展
- 函数的扩展
- 对象的扩展
- Symbol
- Set 和 Map 数据结构
- proxy
- Reflect
- module
- AJAX
- ES5
- 严格模式
- JSON
- 数组方法
- 对象方法
- 函数方法
- 服务端推送(未完成)
- JavaScript应用
- 复杂判断
- 3D 全景图
- 重载
- 上传(未完成)
- 上传方式
- 文件格式
- 渲染大量数据
- 图片裁剪
- 斐波那契数列
- 编码
- 数组去重
- 浅拷贝、深拷贝
- instanceof
- 模拟 new
- 防抖
- 节流
- 数组扁平化
- sleep函数
- 模拟bind
- 柯里化
- 零碎知识点
- 第四部分 进阶
- 计算机原理
- 数据结构(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序
- 搜索算法
- 动态规划
- 二叉树
- 浏览器
- 浏览器结构
- 浏览器工作原理
- HTML解析
- CSS解析
- 渲染树构建
- 布局(Layout)
- 渲染
- 浏览器输入 URL 后发生了什么
- 跨域
- 缓存机制
- reflow(回流)和repaint(重绘)
- 渲染层合并
- 编译(未完成)
- Babel
- 设计模式(未完成)
- 函数式编程(未完成)
- 正则表达式(未完成)
- 性能
- 性能分析
- 性能指标
- 首屏加载
- 优化
- 浏览器层面
- HTTP层面
- 代码层面
- 构建层面
- 移动端首屏优化
- 服务器层面
- bigpipe
- 构建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack优化
- Webpack原理
- 实现loader
- 实现plugin
- tapable
- Webpack打包后代码
- rollup.js
- parcel
- 模块化
- ESM
- 安全
- XSS
- CSRF
- 点击劫持
- 中间人攻击
- 密码存储
- 测试(未完成)
- 单元测试
- E2E测试
- 框架测试
- 样式回归测试
- 异步测试
- 自动化测试
- PWA
- PWA官网
- web app manifest
- service worker
- app install banners
- 调试PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 饿了么整理
- 样式
- 技巧
- Vue音乐播放器
- Vue源码
- Virtual Dom
- computed原理
- 数组绑定原理
- 双向绑定
- nextTick
- keep-alive
- 导航守卫
- 组件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 动画(未完成)
- 异常监控、收集(未完成)
- 数据采集
- Sentry
- 贝塞尔曲线
- 视频
- 服务端渲染
- 服务端渲染的利与弊
- Vue SSR
- React SSR
- 客户端
- 离线包
- 第五部分 网络
- 五层协议
- TCP
- UDP
- HTTP
- 方法
- 首部
- 状态码
- 持久连接
- TLS
- content-type
- Redirect
- CSP
- 请求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服务端
- Linux
- Linux命令
- 权限
- XAMPP
- Node.js
- 安装
- Node模块化
- 设置环境变量
- Node的event loop
- 进程
- 全局对象
- 异步IO与事件驱动
- 文件系统
- Node错误处理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服务
- 负载均衡
- 获取用户IP
- 解决跨域
- 适配PC与移动环境
- 简单的访问限制
- 页面内容修改
- 图片处理
- 合并请求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自动化(未完成)
- docker
- 创建CLI
- 持续集成
- 持续交付
- 持续部署
- Jenkins
- 部署与发布
- 远程登录服务器
- 增强服务器安全等级
- 搭建 Nodejs 生产环境
- 配置 Nginx 实现反向代理
- 管理域名解析
- 配置 PM2 一键部署
- 发布上线
- 部署HTTPS
- Node 应用
- 爬虫(未完成)
- 例子
- 反爬虫
- 中间件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源码
- app.js
- config.js
- 消息队列
- RPC
- 性能优化
- 第七部分 总结
- Web服务器
- 目录结构
- 依赖
- 功能
- 代码片段
- 整理
- 知识清单、博客
- 项目、组件、库
- Node代码
- 面试必考
- 91算法
- 第八部分 工作代码总结
- 样式代码
- 框架代码
- 组件代码
- 功能代码
- 通用代码