前端性能优化

前端性能优化

  • 减少HTTP请求数量
    • CSS Sprites
    • 内联图片(图片base64)
    • 最大化合并JS、CSS模块
    • 利用浏览器缓存
  • 减小HTTP请求大小
    • 压缩HTTP响应包(Accept-Encoding: gzip, deflate)
    • 压缩HTML、CSS、JS模块
  • DOM方面
    • 离线操作DOM
    • 使用innerHTML进行大量的DHTML操作
    • 使用事件代理
    • 缓存布局信息
    • 移除页面上不存在的事件处理程序
  • JavaScript语言本身的优化
    • 使用局部变量代替全部变量,减少作用域链遍历标识符的时间
    • 减少对象成员及数组项的查找次数
    • 避免使用with语句和eval函数
  • ajax优化
    • get或者post请求
    • multipart XHR
    • ajax缓存
  • 其他方面的性能优化
    • 使用CDN加载静态资源
    • CSS样式放在头部
    • JS脚本放在底部
    • 避免使用CSS表达式
    • 外联JS、CSS
    • 减少DNS查找
    • 避免URL重定向

转载请注明出处: 前端性能优化

减少HTTP请求数量

CSS Sprites

将多个图片合并成一张图,只像图片发送一次请求的技术。此时可以通过background-position根据位置定位到不同的图片。虽然合并之后的一张图片包含附加的空白区域,会让人觉得比单个图片合并起来的图片要大。实际上,合并后的图片会比分离的图片的总和要小,因为一来将多次请求合并成了一次,二来降低了图片自身的开销(颜色表,格式信息等等)。

举个例子,如果有需要请求四个25k的图片,那么直接请求100k的图片会比发送四次请求要快一些。因为多次http请求会产生性能开销和图片自身的开销。

内联图片

通过使用data: URL模式可以在Web页面包含图片但无需任何额外的HTTP请求。data: URL中的URL是经过base64编码的。格式如下

<img src="data:image/gif;base64....." alt="home">

由于使用内联图片(图片base64)是内联在HTML中的,因此在跨越页面时不会被缓存。一般情况下,不要将网站的Logo做图片base64的处理,因为编码过的Logo会导致页面变大。可将图片作为背景,放在CSS样式表中,此时CSS可被浏览器缓存

.home {
 background-image: url(data:image/gif;base64.....)
}

最大化JS、CSS的合并

考虑到HTTP请求会带来额外的性能开销,因此下载单个100kb的文件比下载4个25kb的文件更快。最大化合并JS、CSS将会改善性能。

利用浏览器缓存

减少呈现页面时所必需的HTTP请求的数量是加速用户体验的最佳方式。可以通过最大化浏览器缓存组件的能力来实现。

什么是缓存

如果组件(HTML、CSS、JavsScript、图片资源等)被缓存到浏览器中,在下次再次加载的时候有可能从组件中获取缓存,而不是向服务器发送HTTP请求。减少HTTP请求有利于前端性能优化

浏览器如何缓存

浏览器在下载组件(HTML、CSS、JavsScript、图片资源等),会将他们缓存到浏览器中。如果某个组件确实更新了,但是仍然在缓存中。这时候可以给组件添加版本号的方式(md5)避免读取缓存。

浏览器再次下载组件时,如何确认是缓存的组件
1.Expires头

可以通过服务端配置,将某个组件的过期时间设置的长一些。比如,公司Logo不会经常变化等。浏览器在下载组件时,会将其缓存。在后续页面的查看中,如果在指定时间内,表明组件是未过期的,则可以直接读取缓存,而不用走HTTP请求。如果在指定时间外,则表明组件是过期的,此时并不会马上发起一个HTTP请求,而是发起一个条件GET请求。

2.条件GET请求

如果缓存的组件过期了(或者用户reload,refresh了页面),浏览器在重用它之前必须先检查它是否仍然有效。这称为一个条件GET请求。这个请求是浏览器必须发起的。如果响应头部的Last-Modified(最后修改时间,服务器传回的值)与请求头部的If-Modified-Since(最新修改时间)得值匹配,则会返回304响应(Not-Modified),即直接从浏览器中读取缓存,而不是走HTTP请求。

3.Etag(实体标签)

Etag其实和条件GET请求很像,也是通过检测浏览器缓存中的组件与原始服务器上的组件是否匹配。如果响应头部的Etag与请求头部的If-None-Match的值互相匹配,则会返回304响应。

Etag存在的一些问题:

  1. 如果只有一台服务器,使用Etag没有什么问题。如果有多台服务器,从不同服务器下载相同的组件返回的Etag会不同,即使内容相同,也不会从缓存中读取,而是发起HTTP请求。
  2. Etag降低了代理缓存的效率。
  3. If-None-Match比If-Modified-Since拥有更高的优先级。即使条件GET请求的响应头部和请求头部的两个值相同,在拥有多台服务器的情况下,不是从缓存中读取,而是仍然会发起HTTP请求。

有两种方式可以解决这个问题

  1. 在服务端配置Etag。
  2. 在服务端移除Etag。移除Etag可以减少响应和后续HTTP请求头的大小。Last-Modified可以提供完全等价的信息

减少HTTP请求大小

1.组件(HTML, CSS, JavaScript)压缩处理
2.配置请求头部信息:Accept-encoding: gzip, deflate。此时服务器返回的响应头部中会包含Content-encoding: gzip的信息,表明http响应包被压缩。

DOM方面

离线DOM操作

如果需要给页面上某个元素进行某种DOM操作时(如增加某个子节点或者增加某段文字或者删除某个节点),如果直接对在页面上进行更新,此时浏览器需要重新计算页面上所有DOM节点的尺寸,进行重排和重绘。现场进行的DOM更新越多,所花费的时间就越长。重排是指某个DOM节点发生位置变化时(删除、移动、CSS盒模型等),重新绘制渲染树🌲的过程。重绘是指将发生位置变化的DOM节点重新绘制到页面上的过程。

var list = document.getElementById("myList"),
   item,
   i;
for (i=0; i < 10; i++) {
 item = document.createElement("li");
 list.appendChild(item);
 item.appendChild(document.createTextNode("Item " + i));
}

以上元素进行了20次现场更新,有10次是将li插入到list元素中,另外10次文本节点。这里就产生了20次DOM的重排和重绘。此时可以采用以下方法, 来减少DOM元素的重拍和重绘。

一是采用文档碎片(),一是将li元素最后才插入到页面上

一:使用文档碎片(推荐)
var list = document.getElementById("myList"),
   item,
   i,
   frag = document.createDocumentFragment();  // 文档碎片
for (i=0; i < 10; i++) {
 item = document.createElement("li");
 frag.appendChild(item);
 item.appendChild(document.createTextNode("Item " + i));
}
document.body.appendChild(frag)

二:循环结束时插入li
var list = document.getElementById("myList"),
   item,
   i;
for (i=0; i < 10; i++) {
 item = document.createElement("li");
 item.appendChild(document.createTextNode("Item " + i));
}
list.appendChild(item);
采用innerHTML方法

有两种在页面上创建 DOM 节点的方法:使用诸如 createElement()和 appendChild()之类的DOM 方法,以及使用innerHTML。对于小的DOM更改而言,两种方法效率都差不多。然而,对于大的 DOM 更改,使用 innerHTML 要比使用标准 DOM 方法创建同样的 DOM 结构快得多。当把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM 调用来创建 DOM 结构,而非基于JavaScript的DOM调用。由于内部方法是编译好的而非解释执行的,所以执行快得多。

var ul = document.querySelector('ul')
var html = ''
for (var i = 0; i < 10; i++) {
 html += '<li>'+ i +'</li>'
 // 避免在for循环中使用innerHTML, 因为在循环中使用innerHTML会导致现场更新!
}
ul.innerHTML = html   // 循环结束时插入到ul元素中

这段代码构建了一个 HTML 字符串,然后将其指定到 list.innerHTML,便创建了需要的DOM结构。虽然字符串连接上总是有点性能损失,但这种方式还是要比进行多个DOM操作更快。

缓存布局信息

当在实际应用中需要获取页面上某个DOM节点的布局信息时,如offset dimension, client dimension或者是样式等,浏览器为了返回最新值,会刷新整个DOM树去获取。最好的做法是缓存布局信息,减少布局信息的获取次数。获取之后将其缓存到局部变量中,然后再操作此局部变量。

如,需要将某个DOM节点沿对角线移动,一次移动一个像素,从100100 移动到500 500。

如果这样做,对于性能优化来说是低效的。
div.style.left = 1 + div.clientLeft + 'px'
div.style.top = 1 + div.clientTop + 'px'
if (div.style.clientLeft >= 500 && div.style.clientTop >= 500) {
  // 停止累加..
}

下面使用局部变量缓存布局信息,对于性能优化来说是高效的。
let left = div.clientLeft, right = div.clientTop
div.style.left = 1 + left + 'px'
div.style.top = 1 + right+ 'px'
if (div.style.clientLeft >= 500 && div.style.clientTop >= 500) {
  // 停止累加..
}
事件代理

在javascript中,在页面渲染时添加到页面上的事件处理程序数量直接关系到页面的整体运行性能。最直接的影响是页面的事件处理程序越多,访问DOM节点的次数也就越多。另外函数是对象,会占用内存。内存中的对象越多,性能就越差。

事件代理就是解决'过多的事件处理程序'的。事件代理基于事件冒泡机制。因此,可以将同一事件类型的事件都绑定到document对象上,根据事件对象的target属性下的id, class 或者name属性,判断需要给哪个DOM节点绑定事件处理程序。这种事件代理机制在页面渲染时将访问多次DOM节点减少到了一次,因为此时我们只需访问document对象。如下实现

document.addEventListener('click', function (e) {
 switch (e.target.id) {
   case 'new':
     console.log('new')
     break
   case 'name':
     console.log('name')
     break
   case 'sex':
     console.log('sex')
     break
 }
}, false)

使用事件代理有以下优点:

  1. 可以在页面生名周期的任何时间点上添加添加事件处理程序(无需等待DOMContentLoaded和Load事件)。换句话说,只要某个需要添加事件处理程序的元素存在页面上,就可以绑定相应的事件。
  2. DOM节点访问次数减少。
  3. 事件处理程序时函数,而函数是对象。对象会占用内存。事件处理程序减少了,所占用的内存空间就少了,就能够提升整体性能。
移除事件处理程序

假设有这样一个需求:页面上有一个按钮,在点击时需要替换成某个文本。如果直接替换该按钮,由于该按钮的事件处理程序已经存在内存中了,此时移除按钮并没有将事件处理程序一同移除,页面仍然持有对该按钮事件处理程序的引用。一旦这种情况出现多次,那么原来添加到元素中的事件处理程序会占用内存。在事件代理中也谈过,函数是对象,内存中的对象越多,性能有越差。除了文本替换外,还可能出现在移除(removeChild)、替换(replaceChild)带有事件处理程序的DOM节点。

而正确的做法是,在移除该按钮的同时,移除事件处理程序。

<div class="content">
 <button class='btn'>点击</button>
</div>
var btn = document.querySelector('.btn')
btn.addEventListener('click', function func(e) {
 btn.removeEventListener('click', func, false) // 在替换前,移除该按钮的事件处理程序
 document.querySelector('.content').innerHTML = '替换button按钮拉!'
}, false)

JavaScript的优化

使用局部变量代替全局变量,减少在作用域链上搜索标识符的时间

在JavaScript中,作用域分为函数作用域和词法作用域。当我们执行了某个函数时,会创建一个执行环境。如果在执行环境中想搜索某个变量,会经历以下行为:

首先从当前词法作用域开始搜索,如果找到了这个变量,那么就停止搜索,返回该变量;如果找不到,那么就会搜索外层的词法作用域,一直向上冒泡;如果仍然没有在全局作用域下仍然没有搜索到该变量,浏览器就会报RefferceError类型的错误,此错误表示与作用域相关。最后,此函数的执行环境被销毁。

从性能方面思考,如果将某个变量放在全局作用域下,那么读写到该变量的时间会比局部变量多很多。变量在作用域中的位置越深,访问所需时间就越长。由于全局变量总是(document, window对象)处在作用域链的最末端,因此访问速度是最慢的。

举个例子吧。比如我们操作DOM元素时,必不可免的会使用到document对象。这个对象是window对象下的一个属性,也算是一个全局变量吧。因此,当我们操作DOM时,可以将其缓存,作为局部变量存在,那么就避免了作用域链搜索全局变量的过程。

let func = () => {
  let doc = document  // document作为局部变量存在
  let body = doc.body  // body作为局部变量存在
  let p = doc.createElement('p')
  let text = doc.createTextNode('document和body作为局部变量存在')
  body.appendChld(p)
}
减少对象成员数组项的查找次数

这点主要体现在循环体上。以for循环为例,缓存数组长度,而不是在每次循环中获取。

假设有有一个arr数组,长度为50000
// 低效的, 每次都要获取数组长度
for (var i = 0; i < arr.length; i++) {
  // do something...
}
// for循环性能优化:缓存数组长度
for ( var i = 0, len = arr.length; i < len; i++) {
  // do something
}

Ajax方面的优化

get或者post请求

这里可以扯一下get和post请求的区别。

对于get请求来说,主要用于获取(查询)数据。get请求的参数需要以query string的方式添加在URL后面的。当我们需要从服务器获取或者查询某数据时,都应该使用get请求。优点在于gei请求比post请求要快,同时get请求可以被浏览器缓存。缺点在于get请求的参数大于2048个字符时,超过的字符会被截取,此时需要post请求。

对于post请求来说,主要用于保存(增加值、修改值、删除值)数据。post请求的参数是作为请求的主体提交到服务器。优点在于没有字节的限制。缺点是无法被浏览器缓存。

get和post请求有一个共同点:虽然在请求时,get请求将参数带在url后面,post请求将参数作为请求的主体提交。但是请求参数都是以name1=value1&name2=value2 的方式发送到服务器的。

let data ['name1=value1', 'name2=value2']
let xhr = new window.XMLHttpRequest()
xhr.addEventListener('readystatechange', () => {
  if (xhr.readyState === 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      console.log(xhr.responseText)
    }
  }
}, false)
let getPram = '?' + data.join('&')
let postPram = data.join('&')
// open方法:
xhr.open('get', 'url' + getPram, true)
// post方法, 作为请求的主体提交
// xhr.send(postPram)

所以,扯了那么多。要注意的是,get请求用于查询(获取)数据,post请求用于保存(增删改)数据。

跨域JSONP

由于同源政策的限制,ajax只能在同域名、同协议、同端口的情况下才可以访问。也就是说,跨域是不行的。但是可以使用JSONP的方式绕过同源政策。

JSONP实现的原理:动态创建script标签。通过src属性添加需要访问的地址,将返回的数据作为参数封装在回调函数中

let script = document.createElement('script')
script.src = 'url...'
script.id = 'script'
document.head.appendChild(script)

script.addEventListener('load', e => {
  if (this.readyState === 'complete') {
    let data = e
    // do something...
  }
}, false)

JSONP的优点:

  1. 跨域请求。
  2. 由于返回的参数是JavaScript代码,而不是作为字符串需要进一步处理。所以速度快

JSONP的缺点:

  1. 只能以get请求发送。
  2. 无法为错误、失败事件设置事件处理程序。
  3. 无法设请求头。
multipart XHR

暂时未使用过,占位占位、等使用过了再更新:)

ajax缓存

先占位。目前正在开发一个小型类jQuery库。主要目的有:熟悉面向对象编程思想,熟悉DOM操作。到时候开发完ajax模块再回来填坑。

其他方面的性能优化

将样式表放在顶部

CSS样式表可以放在两个地方,一是文档头部,一是文档底部。位置的不同会带来不同的体验。

当样式表放在文档底部时,不同浏览器会出现不同的效果

IE浏览器在新窗口打开、刷新页面时,浏览器会阻塞内容的逐步呈现,取而代之的是白屏一段时间,等到CSS样式下载完毕之后再将内容和样式渲染到页面上;在点击链接、书签栏、reload时,浏览器会先将内容逐步呈现,等到CSS样式加载完毕之后重新渲染DOM树,此时会发生无样式内容的闪烁问题

火狐浏览器不管以什么方式打开浏览器都会将内容逐步呈现,然后等到css样式加载完毕之后再重新渲染DOM树,发生无样式内容的闪烁的问题。

当样式表放在文档顶部时,虽然浏览器需要先加载CSS样式,速度可能比放在底部的慢些,但是由于可以使页面内容逐步呈现,所以对用户来时还是快的。因为有内容呈现了而不是白屏,发生无样式内容的闪烁,用户体验也会友好些。毕竟,有内容比白屏要好很多吧...

将样式放在文档顶部有两种方式。当使用link标签将样式放在head时,浏览器会使内容逐步呈现,但是会发生无样式内容的闪烁问题;当使用@import规则,由于会发生模块(图片、样式、脚本)下载时的无序性,可能会出现白屏的现象。另外,在style标签下可以使用多个import规则,但是必须放置在其他规则之前。link和@import引入样式也存在性能问题,推荐引入样式时都使用link标签。

参看文章:link标签和@import规则的性能区别

文章中,简单的说就是都是用link标签或者都是用@import规则加载CSS样式时会并行下载;而混用link标签和@import规则导致样式无法并行下载,而是逐个下载。由于@import规则会导致模块下载的无序性问题,所以还是推荐全部使用link标签引入css样式

将脚本放在底部

将脚本放在文档顶部会导致如下问题:

  1. 脚本会阻塞其后组件的并行下载和执行
  2. 脚本会阻塞其后页面的逐步呈现

HTTP1.1规定,建议每个浏览器从服务器并行下载两个组件。这也意味着,增加服务器的数量,并行下载的数量也会增加。如果有两台服务器,那么并行下载组件的数量为4。


除了将脚本放在底部可以解决这个以上两个问题,script标签`的async和defer属性也可以解决这两个问题。

asnyc属性(异步脚本)表示脚本可以立即下载,下载完成后自动执行,但不应妨碍页面中的其他操作。比如下载其他模块(图片、样式、脚本)。由于是异步的,所以脚本下载没有先后顺序,没有顺序的脚本就要确保每个脚本不会互相依赖。只对外部脚本文件有效。异步脚本一定会在页面load事件前执行,但可能会在DOMContentLoaded事件触发前后执行。由于async属性可以异步加载脚本,所以可以放在页面的任何位置。

defer属性(延迟脚本)表示脚本可以立即下载,但是会延迟到文档完全被解析和显示之后再执行。在DOMContentLoaded事件之后,load事件之前执行。由于defer属性可以延迟脚本的执行,因此可以放在页面的任何位置。

在没有asnyc属性和defer属性的script标签时,由于js是单线程的原因,所以只能下载完第一个script才能下载第二个,才到第三个,第四个......

避免使用CSS表达式

这个应该很少人用吧...毕竟网上对css表达式介绍的少之又少...反正我是没用过的

外联javascript、css

外联javascript、css文件相对于内联有以下优点。外联的方式可以通过script标签或者link标签引入,也可以通过动态方式创建script标签和link标签(动态脚本、动态样式),此时通过动态方式创建的脚本和样式不会阻塞页面其他组件的下载和呈现。

通用函数
let loadScript = (url, cb) => {
  let script = document.createElement('script')
  支持readystatechange事件的浏览器有IE、Firefox4+和Opera,谷歌不支持该事件。存在兼容性问题。
  if (script.readyState) {
    script.addEventListener('readystatechange', function change () {
      if (script.readyState === 'loaded' || script.readyState === 'complete') {
        // 移除readystatechange,避免触发两次
        script.removeEventListener('readystatechange', change, false)
        cb()
      }
    }, false)
  } else {
    script.addEventListener('load', () => {
      cb()
    }, false)
  }
  script.src = url
  document.body.appendChild(script)
}

// 依次解析和执行a.js、b.js、c.js。
loadScript('./a.js', () => {
  alert('a done')
  loadScript('./b.js', () => {
    alert('b done')
    loadScript('./c.js', () => {
      alert('c done')
    })
  })
})
  1. 可以被浏览器缓存。
  2. 作为组件复用。
减少DNS查找

DNS的作用是将域名解析为IP地址。通常情况下,浏览器查找一个给定主机名的IP地址需要花费20-120ms。在DNS服务器查找完成之前,浏览器不能从服务器那里下载任何东西。减少DNS查找的方法如下。

  1. 减少服务器数量。减少服务器数量意味着并行下载组件的数量也会减少,但是此时会减少DNS查找的时间。应根据具体业务场景做取舍。
  2. 浏览器缓存DNS记录。可以通过服务器配置DNS缓存的时间。
  3. 配置Keep-alive。由于客户端服务器连接是持久的,因此无需DNS查找。
避免url重定向

先占位。

posted @ 2017-08-17 18:36 凯斯keith 阅读(...) 评论(...) 编辑 收藏