Web之前端高性能

    前端的高性能部分,主要是指减少请求数、减少传输的数据以及提高用户体验,本篇前半部分介绍前端图片这块的优化,后半部分介绍包括http协议的利用、动静分离等的优化。

图片压缩

减少图片的大小,可以明显的提高性能,而对于已有图片,要想减少图片的大小,只能改变图片的格式,这里推荐的是 PNG8 的格式,它可以在基本保持清晰度的情况下,减少图片的大小。知道这个原理以后,可以用 Windows 的画图工具、以及 PhotoShop 工具逐个的改变。但是这样做的缺点是单张处理,效率太慢。这里推荐一个YAHOO的在线转换工具 Smush.it,可以批量的进行压缩与转换。它的地址是:www.smushit.com/ysmush.it 。


图像合并实现CSS Sprites

CSS Sprites 是一个吸引人的技术,它其实就是把网页中一些背景图片整合到一张图片文件中,再利用 CSS 的“background-image”,“background- repeat”,“background-position”的组合进行背景定位,background-position 可以用数字能精确的定位出背景图片的位置。利用 CSS Sprites 能很好地减少网页的 HTTP 请求,从而大大的提高了页面的性能,这也是 CSS Sprites 最大的优点,也是其被广泛传播和应用的主要原因。CSS Sprites 能减少图片的字节,由于图像合并后基本信息不用重复,那么多张图片合并成 1 张图片的字节往往总是小于这些图片的字节总和。同时,由于将图片合并到一张图片,因此图片的请求数就被缩减到 1 个。其他的请求都可以用到本地缓存,不需要访问服务器。这里介绍一个小工具 ---“CSS Sprites 样式生成工具 2.0”,可以从 这里下载。


多域名请求

有时候,图片数据太多,一些公司的解决方法是将图片数据分到多个域名的服务器上,这在一方面是将服务器的请求压力分到多个硬件服务器上。另一方面,是利用了浏览器的特性。一般来说,浏览器对于相同域名的图片,最多用 2-4 个线程并行下载。不同浏览器的并发下载数,都是不同的,并发数如下清单所示。

  各浏览器的并发下载数

 Browsers                  HTTP/1.1   HTTP/1.0 
 IE6,7                      2           4 
 IE8                        6           6 
 FireFox 2                  2           8 
 FireFox 3                  6           6 
 Safari 3,4                 4           4 
 Chrome 1,2                 6           ? 
 Chrome 3                   4           4 
 Opera 9.63,10.00alpha      4           4

相同域名的多张图片,它们下载的起始点是存在延迟的。它们并不是并行下载。


图形的base64编码

不管如何,图片的下载始终都要向服务器发出请求,要是图片的下载不用向服务器发出请求,而可以随着 HTML 的下载同时下载到本地那就太好了。而目前,浏览器已经支持了该特性,我们可以将图片数据编码成 BASE64 的字符串,使用该字符串代替图像地址。假设用 S代表这个 BASE64 字符串,那么就可以使用 <img src="data:image/png;base64,S"> 来显示这个图像。可以看出,图像的数据包含在了 HTML 代码里,无需再次访问服务器。那么图像要如何编码成 BASE64 字符串呢?可以使用 在线的工具---“Base64 Online”,这个工具可以上传图片将图片转换为 BASE64 字符串。当然,如果读者有兴趣,完全可以自己实现一个 BASE64 编码工具,比如使用 Java 开发,它的代码就如清单 7 所示。

public static String getPicBASE64(String picPath) {   
        String content = null;   
        try {   
            FileInputStream fis = new FileInputStream(picPath);   
            byte[] bytes = new byte[fis.available()];   
            fis.read(bytes);   
            content = new sun.misc.BASE64Encoder().encode(bytes); // 具体的编码方法   
            fis.close();     
        } catch (Exception e) {   
            e.printStackTrace();   
        }   
        return content;   
 }

从现实我们接触的角度看,静态化的页面中很多小图片是很适合使用BASE64,如一些在线 HTML 编辑器,里面的小图标,如笑脸等,都使用到了 BASE64 编码,因为它们非常小,数量多,BASE64 可以帮助网页减少图标的请求数,提高效率。


GZIP压缩

为了减少传输的数据,压缩是一个不错的选择,而 HTTP 协议支持 GZIP 的压缩格式,服务器响应的报头包含 Content-Encoding: gzip,它告诉浏览器,这个响应的返回数据,已经压缩成 GZIP 格式,浏览器获得数据后要进行解压缩操作。这在一定程度可以减少服务器传输的数据,提高系统性能。那么如何给服务器响应添加 Content-Encoding: gzip 报头,同时压缩响应数据呢?如果你用的是 Tomcat 服务器,打开 $tomcat_home$/conf/server.xml 文件,对 Connector 进行配置,配置如下所示。

 Tomcat配置清单

 <Connector  port ="80"  maxHttpHeaderSize ="8192" 
 maxThreads ="150"  minSpareThreads ="25"  maxSpareThreads ="75" 
 enableLookups ="false"  redirectPort ="8443"  acceptCount ="100" 
 connectionTimeout ="20000"  disableUploadTimeout ="true"  URIEncoding ="utf-8"   
 compression="on" 
 compressionMinSize="2048" 
 noCompressionUserAgents="gozilla, traviata" 
 compressableMimeType="text/html,text/xml" />

我们为 Connector 添加了如下几个属性,他们意义分别是:

compression="on" 打开压缩功能

compressionMinSize="2048" 启用压缩的输出内容大小,这里面默认为 2KB

noCompressionUserAgents="gozilla, traviata" 对于以下的浏览器,不启用压缩

compressableMimeType="text/html,text/xml, image/png" 压缩类型

有时候,我们无法配置 server.xml,比如如果我们只是租用了别人的空间,但是它并没有启用 GZIP,那么我们就要使用程序启用 GZIP 功能。我们将需要压缩的文件,放到指定的文件夹,使用一个过滤器,过滤对这个文件夹里文件的请求。

 自定义Filter压缩清单

// 监视对 gzipCategory 文件夹的请求
 @WebFilter(urlPatterns = { "/gzipCategory/*" }) 
 public class GZIPFilter implements Filter { 

 @Override 
 public void doFilter(ServletRequest request, ServletResponse response, 
 FilterChain chain) throws IOException, ServletException { 
 String parameter = request.getParameter("gzip"); 
 // 判断是否包含了 Accept-Encoding 请求头部
 HttpServletRequest s = (HttpServletRequest)request; 
 String header = s.getHeader("Accept-Encoding"); 
 //"1".equals(parameter) 只是为了控制,如果传入 gzip=1,才执行压缩,目的是测试用
 if ("1".equals(parameter) && header != null && header.toLowerCase().contains("gzip")) { 
 HttpServletResponse resp = (HttpServletResponse) response; 
 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 

 HttpServletResponseWrapper hsrw = new HttpServletResponseWrapper( 
 resp) { 

 @Override 
 public PrintWriter getWriter() throws IOException { 
 return new PrintWriter(new OutputStreamWriter(buffer, 
 getCharacterEncoding())); 
 } 

 @Override 
 public ServletOutputStream getOutputStream() throws IOException { 
 return new ServletOutputStream() { 

 @Override 
 public void write(int b) throws IOException { 
 buffer.write(b); 
 } 
 }; 
 } 

 }; 

 chain.doFilter(request, hsrw); 
 byte[] gzipData = gzip(buffer.toByteArray()); 
 resp.addHeader("Content-Encoding", "gzip"); 
 resp.setContentLength(gzipData.length); 
 ServletOutputStream output = response.getOutputStream(); 
 output.write(gzipData); 
 output.flush(); 
 } else { 
 chain.doFilter(request, response); 
 } 
 } 
 // 用 GZIP 压缩字节数组
 private byte[] gzip(byte[] data) { 
 ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(10240); 
 GZIPOutputStream output = null; 
 try { 
 output = new GZIPOutputStream(byteOutput); 
 output.write(data); 
 } catch (IOException e) { 
 } finally { 
 try { 
 output.close(); 
 } catch (IOException e) { 
 } 
 } 
 return byteOutput.toByteArray(); 
 } 
……
 }

该程序的主体思想,是在响应流写回之前,对响应的字节数据进行 GZIP 压缩,因为并不是所有的浏览器都支持 GZIP 解压缩,如果浏览器支持 GZIP 解压缩,会在请求报头的 Accept-Encoding 里包含 gzip。这是告诉服务器浏览器支持 GZIP 解压缩,因此如果用程序控制压缩,为了保险起见,还需要判断浏览器是否发送 accept-encoding: gzip 报头,如果包含了该报头,才执行压缩。为了验证压缩前后的情况,使用 Firebug 监控请求和响应报头。

GZIP 压缩是一个重要的功能,前面提到的是对单一服务器的压缩优化,在高并发的情况,多个 Tomcat 服务器之前,需要采用反向代理的技术,提高并发度,而目前比较火的反向代理是 Nginx(这在后续的文章会进行详细的介绍)。对 Nginx 的 HTTP 配置部分里增加如下配置。

  Nginx的GZIP配置

 gzip  on; 
 gzip_min_length  1000; 
 gzip_buffers     4 8k; 
 gzip_types       text/plain application/x-javascript text/css text/html application/xml;


懒加载和预加载

预加载和懒加载,是一种改善用户体验的策略,它实际上并不能提高程序性能,但是却可以明显改善用户体验或减轻服务器压力。

预加载原理是在用户查看一张图片时,就将下一张图片先下载到本地,而当用户真正访问下一张图片时,由于本地缓存的原因,无需从服务器端下载,从而达到提高用户体验的目的。为了实现预加载,我们可以实现如下的一个函数。

 预加载函数

 function  preload(callback) {  
 var imageObj = new Image(); 
 images = new Array(); 
 images[0]="pre_image1.jpg"; 
 images[1]=" pre_image2.jpg"; 
 images[2]=" pre_image3.jpg"; 
 for(var i=0; i<=2; i++) { 
   imageObj.src=images[i]; 
  if (imageObj.complete) { // 如果图片已经存在于浏览器缓存,直接调用回调函数
      callback.call(imageObj); 
   } else { 
 imageObj.onload = function () {// 图片下载完毕时异步调用 callback 函数
        callback.call(imageObj);// 将回调函数的 this 替换为 Image 对象
    }; 
 } 
 } 
 } 

 function callback() 
 { 
  alert(this.src + “已经加载完毕 , 可以在这里继续预加载下一组图片”); 
 }

上面的代码,首先定义了 Image 对象,并且声明了需要预加载的图像数组,然后逐一的开始加载(.src=images[i])。如果已经在缓存里,则不做其他处理;如果不在缓存,监听 onload 事件,它会在图片加载完毕时调用。

而懒加载则是在用户需要的时候再加载。当一个网页中可能同时有上百张图片,而大部分情况下,用户只看其中的一部分,如果同时显示上百张,则浪费了大量带宽资源,因此可以当用户往下拉动滚动条时,才去请求下载被查看的图像,这个原理与 word 的显示策略非常类似。

在 JavaScript 中,它的基本原理是首先要有一个容器对象,容器里面是 img 元素集合。用隐藏或替换等方法,停止 img 的加载,也就是停止它去下载图像。然后历遍 img 元素,当元素在加载范围内,再进行加载(也就是显示或插入 img 标签)。加载范围一般是容器的视框范围,即浏览者的视觉范围内。当容器滚动或大小改变时,再重新历遍元素判断。如此重复,直到所有元素都加载后就完成。当然对于开发来讲,选择已有的成熟组件,并不失为一个上策,Lazy Load Plugin for jQuery 是基于 JQuery 的懒加载组件,它有自己的 官方网站。这是一个不错的免费插件。可以帮助程序员快速的开发懒加载应用。


Flush机制的使用

实际上在 Web 技术中,Flush 机制并不新鲜,它的思想是无需等到网页内容全部加载完毕,一次性写回客户端,而是可以部分逐次的返回。如果网页很大的话,一次性写回全部内容显然是个不明智的选择,因为这会造成网页的长时间空白。Flush 机制允许开发人员将网页的内容按文档流顺序逐步返回给客户端,这样可以使得用户知道我们的系统正在工作,只是等待的时间稍长而已,这样用户也会“心甘情愿”的等下去。Flush 机制是一个经典的提高用户体验的方法,至今也一直在用。如果网页很大,这个机制也是建议使用的。在 Java Web 技术中,实现 Flush 非常简单,只要调用 HttpServletResponse.getWriter 输出流的 flush 方法,就可以将已经完成加载的内容写回给客户端。

但是是否每个网页都要使用该技术呢?笔者当然不这么建议。将网页内容加载完毕后再一次性返回客户端也有它的好处。我们知道网络传输也有最大的传输单元,内容加载完毕后一次性输出就可以最大程度的利用传输的带宽,减少分块,减少传输次数,也就是说实际上 Flush 机制会增加用户等待时间、增加浏览器渲染时间,但是对于大网页来说,降低这点效率来增强用户体验,是值得的。


动静分离

所谓的动静分离,就是将 Web 应用程序中静态和动态的内容分别放在不同的 Web 服务器上,有针对性的处理动态和静态内容,从而达到性能的提升。Java Web 的主流服务器软件是 Tomcat,让人遗憾的是,Tomcat 在并发和静态资源处理的能力上较弱。可采用Apache+Tomcat及Nginx+Tomcat的动静分离结构,如下图:

       Web之前端高性能

将静态资源放在 A 主机的一个目录上,将动态程序放在 B 主机上,同时在 A 上安装 Nginx 并且在 B 上安装 Tomcat。配置 Nginx,当请求的是 html、jpg 等静态资源时,就访问 A 主机上的静态资源目录;当用户提出动态资源的请求时,则将请求转发到后端的 B 服务器上,交由 Tomcat 处理,再由 Nginx 将结果返回给请求端。

  动静分离的Nginx配置

# 转发的服务器,upstream 为负载均衡做准备
 upstream tomcat_server{ 
        server 192.168.1.117:8080; 
 } 

 server { 
        listen       9090; 
        server_name  localhost; 
 index index.html index.htm index.jsp; 
        charset koi8-r; 

        # 静态资源存放目录
        root  /home/wq243221863/Desktop/ROOT; 

        access_log  logs/host.access.log  main; 

 # 动态请求的转发
        location ~ .*.jsp$ { 
            proxy_pass http://tomcat_server; 
            proxy_set_header Host $host; 
        } 
        
 # 静态请求直接读取
 location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|css)$ { 
          expires      30d; 
 } 
……


HTTP持久连接

持久连接(Keep-Alive)也叫做长连接,它是一种 TCP 的连接方式,连接会被浏览器和服务器所缓存,在下次连接同一服务器时,缓存的连接被重新使用。由于 HTTP 的无状态性,人们也一直很清楚“一次性”的 HTTP 通信。持久连接则减少了创建连接的开销,提高了性能。HTTP/1.1 已经支持长连接,大部分浏览器和服务器也提供了长连接的支持。

可以想象,要想发起长连接,服务器和浏览器必须共同合作才可以。一方面浏览器要保持连接,另一方面服务器也不会断开连接。也就是说要想建立长连接,服务器和浏览器需要进行协商,而如何协商就要靠伟大的 HTTP 协议了。它们协商的结构图如图 所示。

     Web之前端高性能

浏览器在请求的头部添加 Connection:Keep-Alive,以此告诉服务器“我支持长连接,你支持的话就和我建立长连接吧”,而倘若服务器的确支持长连接,那么就在响应头部添加“Connection:Keep-Alive”,从而告诉浏览器“我的确也支持,那我们建立长连接吧”。服务器还可以通过 Keep-Alive:timeout=10, max=100 的头部告诉浏览器“我希望 10 秒算超时时间,最长不能超过 100 秒”。

在 Tomcat 里是允许配置长连接的,配置 conf/server.xml 文件,配置 Connector 节点,该节点负责控制浏览器与 Tomcat 的连接,其中与长连接直接相关的有两个属性,它们分别是:keepAliveTimeout,它表示在 Connector 关闭连接前,Connector 为另外一个请求 Keep Alive 所等待的微妙数,默认值和 connectionTimeout 一样;另一个是 maxKeepAliveRequests,它表示 HTTP/1.0 Keep Alive 和 HTTP/1.1 Keep Alive / Pipeline 的最大请求数目,如果设置为 1,将会禁用掉 Keep Alive 和 Pipeline,如果设置为小于 0 的数,Keep Alive 的最大请求数将没有限制。也就是说在 Tomcat 里,默认长连接是打开的,当我们想关闭长连接时,只要将 maxKeepAliveRequests 设置为 1 就可以。


HTTP协议的合理使用

很多程序员都将精力专注在了技术实现上,他们认为性能的高低完全取决于代码的实现,却忽略了已经成型的某些规范、协议、工具。最典型的就是在 Web 开发上,部分开发人员没有意识到 HTTP 协议的重要性,以及 HTTP 协议可以提供程序员另一条性能优化之路。通过简单的在 JSP 的 request 对象中添加响应头部,往往可以迅速提升程序性能。

浏览器缓存带来的性能提升已经众人皆知了,而很多人却并不知道浏览器的缓存过期时间、缓存删除、什么页面可以缓存等,都可以由我们程序员来控制,只要您熟悉 HTTP 协议,就可以轻松的控制浏览器。


CDN的使用

所谓的 CDN,就是一种内容分发网络,它采用智能路由和流量管理技术,及时发现能够给访问者提供最快响应的加速节点,并将访问者的请求导向到该加速节点,由该加速节点提供内容服务。利用内容分发与复制机制,CDN 客户不需要改动原来的网站结构,只需修改少量的 DNS 配置,就可以加速网络的响应速度。当用户访问了使用 CDN 服务的网站时,DNS 域名服务器通过 CNAME 方式将最终域名请求重定向到 CDN 系统中的智能 DNS 负载均衡系统。智能 DNS 负载均衡系统通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的节点地址提供给用户,使用户可以得到快速的服务。同时,它还与分布在不同地点的所有 CDN 节点保持通信,搜集各节点的健康状态,确保不将用户的请求分配到任何一个已经不可用的节点上。而我们的 CDN 还具有在网络拥塞和失效情况下,能拥有自适应调整路由的能力。

由于笔者对 CDN 没有亲身实践,不便多加讲解,但是各大网站都在一定程度使用到了 CDN,淘宝的前端技术(将众多的商品图片CDN化,Cache化)演讲中就提及了 CDN,可见 CDN 的威力不一般。


你可能感兴趣的:(前端高性能)