第二篇,前端高性能JavaScript优化

  1. 加载和执行
    JavaScript是单线程,所以JavaScript的加载和执行是从上下文加载执行完一个继续加载执行下一个文件会阻塞页面资源的加载,所以一般情况下JavaScript文件放在body标签内底部,很多后端开发人员放在body标签外下面,这样做不好的地方有两点:1、不规范 2、可能会造成js获取不到页面元素而导致报错。而放在body标签内底部可以去保js执行前有页面渲染完成。
<body> js... //正确 </body> <!-----------------------分界线----------------------------> <body> js...
合并脚本每个<script>标签初始化下载都会阻塞页面渲染,所以减少页面的<script>标签数量可以起到优化作用,内嵌脚本外链脚本通用,另外HTTP会带来额外的性能消耗,下载一个100KB的文件比下载4个25KB的文件更快,所以可以通过进行脚本合并。 1、减少<script>标签数量。 2、减少HTTP请求带来的消耗(针对外链脚本),虽然现在很多工具帮我们完成了合并工作,但是原理是需要了解的。

特殊情况:把一段内嵌的脚本紧跟在外链css标签后面,会导致:内嵌脚本为了确保执行的时候获得的是最精准的样式信息,会去先等待外链样式表达的下载,这样会导致页面阻塞去等待该样式表下载。如下:

//错误示范

三种无阻塞下载JavaScript的方法:
1. 使用<script>标签的defer属性,defer描述:规定是否对脚本执行进行延迟,直到页面加载为止。
2. 使用动态创建的<script>元素来下载并执行代码,俗称动态脚本注入,建议:动态加载的脚本放在head标签中比放在body标签中更保险,尤其是在页面加载中执行脚本,当body的内容没有全部加载完成时,IE可能会报错

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'a.js';
document.querySelectorAll('head')[0].appendChild(script);

  1. 数据存取
    JavaScript四种基本数据存取位置:
    1.字面量:代表自身,无特定位置,包括:字符串、数字、不尔值、对象、正则表达式及 null 和 undefined;
    2.本地变量:var | let | const 关键在定义的数据存储单元
    3.数组元素:储存在JavaScript数组对象内部,字符串为索引,下班从0开始
    4.对象成员:储存在JavaScript对象内部,以字符串为索引
    从一个字面量和本地变量中储存数据时的性能消耗极小(可忽略),数组和对象则稍微高一些。
    建议:尽量使用字面量和局部变量(局部变量在方法运行过后会自行释放,用完手动置为null 或 undefined也行),减少使用对象和数组,比如某作用域内的值被函数引用一次以上,就可以把它存储到局部变量中来使用、
    var doc = document.querySelectorAll... ,
    a = doc.getElement... ,
    b = doc.getElement... ,
    c = doc.getElement... ;

    作用域
    每一个函表示为一个对象,是function的有个实力,function对象提供一个内部属性[[scope]]该属性仅js引擎读取(题外话:想起来css的scoped属性,在vue组件中使用时,该样式文件作用于该组件)[[scoped]]包含一个函数被创建的作用域中对象的集合,这个集合就是函数的作用域链,当执行环境创建时,作用域链初始化为[[scoped]]属性中的对象,按照出现顺序,复制到执行环境的作用域,当后执行环境会创建一个'活动对象','活动对象'作为函数运行的变量对象,包含所有局部变量、命名参数、参数集合和this,当执行环境销毁,活动对象也被销毁。

    在函数执行时,遇到一个变量,都会经历一场标识符解析过程(标识符位置越深,对应变量读写慢)
    去决定从哪获取储存数据。
    大个比方,A作用域包含B作用域包含C作用域,在C作用域中根据一个标识符去找对应的变量,如果在C作用域中没有找到,就会在去B作用域搜索,B作用域没找到,再去A作用域搜索。搜索的过程销毁了性能,依然是上面的例子,document是全局变量,搜索该变量时是在最后全局变量中找到的,所以放在全局变量中会更快找到。
    TIPS:命名相同的两个变量存在于作用域链的不同部分,则标识符是遍历作用域时先找到的那个,先找到的屏蔽后面的。各浏览器标识符解析速度有差异。
    建议:尽可能使用全局部变量

    with语句和try catch语句可以改变作用域链,尽量避免使用with语句来改变作用域链,因为它使函数的所有局部变量出现在第二个作用域链对象中。
    function aa() {
    with(document){ //访问document变快了,访问其他局部变量变慢了
    var bd = body,
    links = getElementsByTagName('a'),
    i = 0,
    len = links.length;
    ...
    }
    }

然后try catch是把try快里面的错误抛到了catch快中处理,也是改变了作用域链,dangcatch快语句执行完毕后,作用域链又会恢复到之前状态

动态作用域

with  try  catch、还是包含eval()的函数,都可以认为是动态的作用域,但都是执行过程中才有动态作用域,无法通过查看代码结构检测出来,只有在必要时才推荐使用动态作用域

闭包

function assignEvents() {
var id = '2341321';
document.querySelectorAll('#aaa').onclick = function (event) {
saveDom(id);
}
}
var idNew = id;

作为局部变量,id只能在当前作用域访问,出了作用域就访问不到了,但是这个事件处理函数处于该作用域内,可以获取局部变量id,就变成了assignEvents外的saveDom方法可以获得assignEvents的局部变量id。为了实现这一功能。
闭包的[[scope]]属性引用了assignEvents执行环境作用域链的对象(这个对象包含id属性),当执行结束时,执行环境销毁,理应活动对象也被销毁,但是因为闭包的引入,导致这个活动对象处于激活状态,就无法销毁,这就需要更多的内存空间。
由于IE使用非原生JavaScript对象实现DOM对象,所以闭包可能导致内存泄漏。
由于saveDom方法跨作用域访问变量id,所以闭包会带来性能消耗,解决办法是:常用跨作用域变量储存在局部变量中使用

对象成员

大部分JavaScript代码是面向对象编写的(自定义对象、BOM/DOM),所以会导致非常频繁的访问对象成员。所以访问对象也可优化的地方
嵌套成员:对象可以嵌套其他成员
嵌套深度与读取时间成正比

原型链

3.DOM编程(常见的性能瓶颈)

三个问题:
1.访问和修改DOM元素
2.修改DOM元素样式导致的重绘和重排
3.通过DOM事件处理与用户交互

DOM

DOM:document object module 文档对象模型,可以理解为操作文档的程序接口
为什么说DOM慢,操作DOM代价昂贵,简单理解就是两个独立的功能只要通过接口彼此链接,就会产生消耗。比如:中国人买iPhone,美国人买卫龙,需要交税的,这个税就是消耗,同样,从DOM到JavaScript或从JavaScript到DOM都有类似的消耗。
所以尽量的减少这种交税的次数来达到一定的性能优化,
最坏的方式就是在循环中操作或者访问DOM,非常消耗性能。

//bad
for(var i = 0; i < 10000; i++){
document.querySelectorAll('#aaa').innerHTML += 'a';
}
//good
var aaaHtml = '';
for(var i = 0; i < 10000; i++){
aaaHtml += 'a';
}
document.querySelectorAll('#aaa').innerHTML += aaaHtml;

关于innerHTML和DOM方法(doc.creatrElement())谁更快

不考虑web标准的情况下,差不多、
除了最新版本Webkit内核之外的浏览区中,innerHTML更快,旧版本浏览器效率更高
新版的WebKit内核的浏览区DOM方法更快
克隆节点带来的优化效果不是很明显、略过
访问集合元素时使用局部变量(跟操作一个元素多次是一个道理,不赘述)

·遍历DOM

一般来说,querySelectorAll()是获取元素最快的API返回的是一个NodeList
querySelect()返回的是element。
querySelectAll()还有一点就是可以同时获取两类元素
var two = doc.querySelectorAll('div.aaa,div.bbb');

·重绘和重排
浏览区下载完页面中的所以组件--HTML标记、JavaScript、CSS、图片之后会解析成两个内部数据结构:
1.DOM数 表示页面结构 比如操场上早操,小红你站这,小绿你站那,小白...(滚出去),哈,开个玩笑,这种位置的结构就像DOM数
2.渲染树 表示DOM节点如何显示 比如小红穿绿衣服,小绿穿红衣服,小白穿毛呢大衣 小红长头发小绿绿头发等等

DOM数中每一个需要显示的节点在渲染树种至少存在一个对应的节点(隐藏元素没有对应节点,所以可以利用这一点,先把元素隐藏然后处理然后显示来优化消耗的性能),渲染树中的节点被称为’帧‘或者’盒‘,符合css模型定义。当DOM和渲染树构建完成,浏览区开始显示页面元素。哪什么时候开始重绘和重排呢:
当DOM变化影响了几何属性,浏览区会让渲染树中收影响的部分失效,重新构造渲染树。这个过程称为重排;比如班级座位正常,某段时间后小明狂胖200斤,本来小明座一个位置,现在需要两个位置,其他同学就需要往两边座或者往后坐,当然,小明会不会滚出去取决于小明的成绩好坏。
完成重排后,,浏览区会把受影响的部分重新绘制到屏幕上,这个过程就称为重绘;
当改变DOM的非几何属性时,只会发生重绘,不会重排;
重绘和重排都是代价昂贵的;尽量减少

重排何时发生:
1.添加或删除可见DOM元素
2.元素位置改变
3.元素尺寸改变(内外边框、边框宽度高等)
4.内容改变(内容导致尺寸变化的时候)
5.页面渲染器初始化
6.浏览区窗口尺寸变化

减少重绘和重排

//三次重绘
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';

//一次重绘
el.style.cssText = 'border-left: 1px;border-right: 2px; padding: 5px';

批量修改DOM时如何减少重绘和重排: 步骤:
1.使元素脱离文档流
2.对其应用多重改变
3.把元素带回文档中 //步骤1 3 两次重排

三种方法使DOM脱离文档:
1.隐藏元素--应用修改--显示
2.使用文档片断,在当前DOM之外构建一个子树,再拷回文档
3.拷贝到一个脱离文档的节点中,修改副本,副本替换原始元素

·让元素脱离动画流
    一般来说,重绘只会影响一小部分渲染树,但是也是有可能影响很大一部分甚至全部。一次大规模的重排可能会让用户觉得页面一顿一顿的,影响用户体验
避免大部分重排:元素使用绝对定位让其脱离文档流--动画--恢复定位

·IE和:hover
从IE7开始,IE可以在任何元素上使用:hover这个伪选择器,但是当你大量元素使用时 会降低影响速度IE8更明显

·事件委托:事件逐成冒泡被父级捕获

每绑定一个事件处理器都是有代价的
事件三阶段:捕获--到达目标--冒泡
事件委托的兼容问题:访问事件对象、判断事件源、取消冒泡(可选)、阻止默然动作(可选)
使用事件委托来减少事件处理器的数量

4.算法和流程控制
·循环
大多数编程语言中,代码执行时间大部分消耗在循环中,所以循环也是提升性能的重要环节之一

JavaScript四种循环:
1.for循环
Tips:for循环初始化会创建一个函数级变量而不是循环级,因为JavaScript只有函数级作用域(ES6存在快级作用域if(){let n = ...} let定义的n只作用于if快内部,执行完就会释放不会导致变量提升,所以在for循环中定义一个变量和在循环体外定义一个变量时一样的)
var i = 100;
for(var i = 0;i < 10; i++){
    console.log(i)
}

2.while循环
3.do-while循环
4.for in循环
Tips: for in循环可以枚举任何对象的属性名(不是值),但是for in比其他三个循环明显要慢,所以除非要迭代一个属性数量未知的对象,否则避免使用for in 循环,如归遍历一个属性数量已知属性列表,其他循环要比for in快,
比如:
var arr = ['name','age'],
    i = 0;
while(i < arr.length){
    process(object[arr[i]]);
}

假设以上四种循环类型性能一样,可以从两个方面去优化循环的性能:
(当循环体复杂度为x时,优化方案优化减少循环体的复杂度,循环体复杂度大于x时,优化方案优化减少迭代次数)
1.每次迭代的事务(减少循环体的复杂度)
2.迭代的次数(减少循环的次数,白度’达父设备‘),可以这么理解,达夫设备就是拆解循环,比如遍历一个长度为100的数组,普通情况下循体执行100次,达夫设备的思想是把100次拆为每次循环执行对此(n表示)100对n取余,执行取余次数,在执行100除以n(下舍)次循环,这个循环体执行n次普通循环体的操作
达夫设备代码:(这个8就是我说的n)
var i = items.length % 8;           //先循环余数次数
while(i){
    process(items[i--]);
}
i = Math.floor(items.length / 8);   //再循环8的整数倍次数  循环体是普通循环的8倍 可以写成函数传参调用
while(i){
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
}

最小化属性查找:

for(var i = 0, len = arr.length; i < len; i++){
...
}
基于函数的迭代:forEach()
forEach遍历一个数组的所有成员,并执行一个函数
arr.forEach(function(value, index, array){
...
})

但是所有情况下。基于循环的迭代比基于函数的迭代快8倍,在运行速度要求严格时,基于循环的迭代优先于基于函数的迭代

条件语句
if-else对比switch:
当条件较少时 使用if-else更易读,而当条件较多时if-else性能负担比switch大,易读性也没switch好。
优化if-else的方法是:尽可能的把可能出现的条件放在首位,比如:

var i = Math.random(1);
if(i <= 0.8){ //i小于0.8是几率最大的,如果i的值满足i <= 0.8 后面的条件就不会再判断了
...
}else if(i > 0.8 && i <= 0.9){
...
}else{
...
}

当条件很多的时候:(比如10个和10个以上),避免使用条件语句if-else、switch是最佳方式是使用
hash表

    Memoization
减少工作量就是最好的性能优化技术(你可以理解为,砍需求是为了性能优化,这是鲁迅说的--鲁迅:这句话我还真说过)
Memoization避免重复工作,缓存前一个计算的结果为后面的计算所用
比如分别求4、5、6的阶乘
求6的阶乘的时候,因为我缓存了5的阶乘结果,那么6的阶乘就是5的阶乘结果乘以6
<!--Memoization缓存重复运算的值-->
        function memoizeA(n) {
            if(!memoizeA.cache){
                memoizeA.cache = {
                    '0': 1,
                    '1': 1
                }
            }
            if(!memoizeA.cache.hasOwnProperty(n)){
                memoizeA.cache[n] = n * memoizeA(n-1)
            }
            return memoizeA.cache[n]
        }

        var a1 = memoizeA(4)
        console.log(a1)          //24
        var a2 = memoizeA(5)
        console.log(a2)            //120
        var a3 = memoizeA(6)
        console.log(a3)           //720

        <!--封装为方法-->
        function memoize(func, cache) {
            cache = cache || {};
            
            var shell = function (arg) {
                if(!cache.hasOwnProperty(arg)){
                    cache[arg] = func(arg);
                }
                return cache[arg];
            }
            return shell;
        }
        var funCcc = function ccc(n){
            if(n == 0){
                 return 1;
            }else{
                return n*ccc(n-1)
            }
        }
        var a4 = memoize(funCcc,{"0":1,"1":1});
        console.log(a4(6));         //720
  1. 字符串和正则表达式
    正则表达式

    字符串
    比较一下四种字符串拼接方法和性能:
    A:str = str + 'a' + 'b'
    B:str += 'a' + 'b'
    C: arr.join('')
    D: str.conncat('b', 'c')

    对于A与B比较:B会在内存中创建一个临时字符串,字符串拼接为‘ab’后赋值给临时字符串,临时字符串赋值给str;大多数浏览器下A优于B,但在IE8及更早的版本中,B优于A
    关于join、concat加前两种拼接的效率:
    //+=
    (function () {
    var startTime = new Date().getTime();
    var str = '';
    var addStr = 'hello world~, hello xiaojiejie';
    for(var i = 0; i < 100000; i++){
    str += addStr;
    }
    var endTime = new Date().getTime();
    console.log('字符串str += a:');
    console.log(endTime-startTime);
    })();
    // +
    (function () {
    var startTime = new Date().getTime();
    var str = '';
    var addStr = 'hello world~, hello xiaojiejie';
    for(var i = 0; i < 100000; i++){
    str = str + addStr;
    }
    var endTime = new Date().getTime();
    console.log('字符串str = str + a:');
    console.log(endTime-startTime);
    })();
    //concat
    (function () {
    var startTime = new Date().getTime();
    var str = '';
    var addStr = 'hello world~, hello xiaojiejie';
    for(var i = 0; i < 100000; i++){
    str = str.concat(addStr);
    }
    var endTime = new Date().getTime();
    console.log('字符串str.concat:');
    console.log(endTime-startTime);
    })();
    //join
    (function () {
    var startTime = new Date().getTime();
    var str = '';
    var arr = [];
    var addStr = 'hello world~, hello xiaojiejie';
    for(var i = 0; i < 100000; i++){
    arr.push(addStr);
    }
    str = arr.join('');
    var endTime = new Date().getTime();
    console.log('字符串join:');
    console.log(endTime-startTime);
    })();

    我用这段代码简单在Chrome上测试了下,平均下来A>B>C>D,未统计取平均,也没测试其他浏览器
    书上说在IE老版本join是比较快的,也是大量字符串拼接的唯一高效方式

6.快速相应的用户界面

·浏览器ui线程
用于执行JavaScript和更新用户界面的进程被称为'浏览器UI线程',UI线程的工作基于一个队列系统,当进程空闲时,就会从改队列提取任务去执行,改任务可能是JavaScript代码也可能是UI更新(重绘、重排)
UI:用户界面GUI: 
![](https://img2018.cnblogs.com/blog/1625728/201908/1625728-20190801161351615-1076650242.jpg)

浏览器限制JavaScript任务的运行时间,限制两分钟,可以防止恶意代码不断执行来锁定你的浏览器。
单个JavaScript操作的花费总时间应该小于等于100ms,这就意味在100ms内响应用户的操作,不然就会让用户感受到迟钝感

·定时器让出时间片段
    
如果代码复杂100ms运营不完,可以使用定时器让出时间片断,从而使UI获得控制权进行更新。

这个例子只是说明JavaScript单线程,定时器可以把任务放到后面执行,方便理解

console.log(111);
setTimeout(func(){console.log(222)},0);
console.log(333);
//111 333 222

JavaScript是单线程,所以定时器可以把JavaScript任务放到后面,控制权限先交给UI线程
定时器进度有几毫秒的偏差,,window系统中定时器的分辨率25ms所以建议延时最小值置为25ms

把一个任务分解成一系列任务
把一个运行时间长的函数分解为一个个短时间的子函数

使用时间戳计算程序运行时间,1秒及以上的低频定时器不会有什么影响,当使用高频100ms-200ms之前的定时器时响应会变慢,所以高频重复定时器使用要注意

·Web Workers(HTML5新特性)

在UI线程外运行,不占用UI线程的时间
Web Workers不能修改demo

运行环境组成:
一个navigtor对象
一个navigtor对象(与window.location相同 属性-只读)
一个self对象,指向worker对象
可以引入需要用到的外部文件importScript()方法
可以使用js对象Object、Array、Date等
XHR
定时器
close()立刻停止worker运行

实际应用场景:处理纯数据或者与UI线程无关的长时间运行脚本,个人觉得大量的纯计算可以考虑使用

7.Ajax
前面说到数据存取会影响性能,理所应当的,数据的传输同样影响性能
Ajax通过异步的方式在客户端和服务端之间传输数据

    ·数据传输

请求数据的五种方式:
A:XMLHTTPRequest(简称XML)
最常用异步发送和接收数据,包括GET和POST两种方式。
不能跨域
GET--参数放在url后面,请求得到数据会被缓存,当url加参数超过2048,可以使用POST方式
POST--参数在头信息,参数不会被缓存

B:动态脚本注入
其实就是创建一个script元素这个元素的src不受当前域限制,但是不能设置请求头信息,也就是只能用GET方式

C.Multipart XHR
MXHR荀彧一个HTTP请求就可以传输多个数据
通过在服务端讲资源打包成一个双方约定的字符串分割的长字符串发送到客户端,然后根据mime-typed类型和传入的其他头信息解析出资源
缺点:资源不能被缓存

D.iframe
E.comet

发送数据:XHR、Beacons、

    ·数据格式
A:XML
优点:通用、格式严格、易于验证

B:JSON
JSON.parse(); JSON -> 对象
JSON.stringify(); js值 -> JSON对象
文件小、下载快、解析快

C:JSON-P
在客户端注册一个callback,然后把callback的名字传递给服务器。此时,服务器先生成json数据。然后以JavaScript 语法的方式,生成一个function,function名字就是传递上来的参数jsonp。最后将jsonp数据直接以入参的方式,放置到function中,这样就生成了一段js语法的文档,返给客户端。
D:HTML
E:自定义数据格式

·Ajax性能
最快的Ajax请求就是没有请求(贫一句:最快的写程序方式就是天天跟产品拌嘴,砍需求,那啥,我先跑了,产品拿着刀追来了)

避免不必要的请求:
服务端设置HTTP头信息确保响应会被浏览器缓存
客户端讲获取的信息存到本地避免再次请求(localstorage sessionstorage cookice)
设置HTTP头信息,expiresgaosu告诉浏览器缓存多久
减少HTTP请求,合并css、js、图片资源文件等或使用MXHR
通过次要文件用Ajax获取可缩短页面加载时间

8.编程实践
·避免双重求值
eval()、Function慎用,定时器第一个参数建议函数而不是字符串都能避免字符串双重求值

·使用对象或者数组直接量
直接量:

var obj = {
name:...
age:...
}
非直接量:
var obj = new Object()
obj.name = ...
...

运行时直接量比非直接量快

·避免重复工作
A:延迟加载(懒加载)
进入函数->判断条件->重写函数
B:条件预加载
函数提前进行条件检测
var addEvent = doc.addEventListener ? funcA : funcB
    ·使用JavaScript速度快的部分

A.位操作
B.原生方法,首先原生方法是最快的,而且浏览器会缓存部分原生方法
C.复杂计算时多使用Math对象
D.querySelector和querySelectorAll是查询最快的
当用Document类型调用querySelector()方法时,会在文档元素范围内查找匹配的元素;而当用Element类型调用querySelector()方法时,只会在这个元素的后代元素中去查找匹配的元素。若不存在匹配的元素,则这两种类型调用该方法时,均返回null。
  1. 构建并部署高性能JavaScript应用
    这一章讲的都是其他章节的优化原理的实践,主要有:
    1.合并多个js文件
    2.预处理js文件
    3.js压缩
    4.js的HTTP压缩
    5.缓存js文件
    6.处理缓存问题
    7.使用内容分发网络(CDN)这个有点效果显著的感觉,前年第一次用的时候感觉快了很多,打个比方就是:
    京东网上水果蔬菜超市,假设你在上海买了一个榴莲,京东可以在上海的仓库给你发货,如果上海没有他们的仓库,就在离你最近的一个仓库发货,以保证最快速度送到你手上(吃什么不好,吃榴莲,别人会说食屎拉你)。这个仓库放的就是静态资源文件,根据请求
    发出的位置找到最近的CDN节点把资源返回给请求端,大概是这个意思,具体原理参考CDN原理
    现在很多方式都在gulp、webpack工具里进行了,方便省事

  2. 工具
    JavaScript性能分析
    使用Date对象实例减去另一个实例获得任务运行时间毫秒数

    匿名函数
    测量分析匿名函数的方法就是给匿名函数加上名字

    调试工具
    个人比较喜欢chrome调试工具
    贡献几个比较全的教程
    基础篇 https://www.cnblogs.com/st-leslie/p/8196493.html
    优化篇 https://www.cnblogs.com/st-leslie/p/8244559.html
    实战1 https://www.cnblogs.com/st-leslie/p/8261319.html
    实战2 https://www.cnblogs.com/st-leslie/p/8271347.html

    脚本阻塞
    Safari4、IE8、Firefox3.5、chrome及以上允许脚本并行下载,但阻塞运行,虽然文件下载快了,但是页面渲染任会阻塞直到脚本运行完
    对运行慢的脚本进行优化或重构,不必要的脚本等到等到页面渲染完成再加载

    Page Speed
    显示解析和运行JavaScript消耗的时间,指明可以延长加载的脚本,并报告没被使用的函数

    Fiddler
    Fiddler是一个HTTP调试代理工具,能检测到网络中所有资源,以定位加载瓶颈

    YSlow
    YSlow工具可以深入观察页面初始加载和运行过程的整体性能

    WebPagetest
    WebPagetest:根据用户浏览器真实的连接速度,在全球范围内进行网页速度测试,并提供详细的优化建议。
    https://www.webpagetest.org/

    Google PageSpeed
    PageSpeed 根据网页最佳实践分析和优化测试的网页。

    Pingdom 网站速度测试
    输入 URL 地址,即可测试页面加载速度,分析并找出性能瓶颈。
    https://tools.pingdom.com/

posted @ 2019-08-01 17:38  limeiwang  阅读(213)  评论(0编辑  收藏  举报