俄罗斯贵宾会-俄罗斯贵宾会官网
做最好的网站

前端性能优化 —— 前端性能分析

前端性能优化 —— 前端性能分析

2018/01/11 · 基础技术 · 移动端

原文出处: ouven   

前端性能优化是一个很宽泛的概念,本书前面的部分也多多少少提到一些前端优化方法,这也是我们一直在关注的一件重要事情。配合各种方式、手段、辅助系统,前端优化的最终目的都是提升用户体验,改善页面性能,我们常常竭尽全力进行前端页面优化,但却忽略了这样做的效果和意义。先不急于探究前端优化具体可以怎样去做,先看看什么是前端性能,应该怎样去了解和评价前端页面的性能。

通常前端性能可以认为是用户获取所需要页面数据或执行某个页面动作的一个实时性指标,一般以用户希望获取数据的操作到用户实际获得数据的时间间隔来衡量。例如用户希望获取数据的操作是打开某个页面,那么这个操作的前端性能就可以用该用户操作开始到屏幕展示页面内容给用户的这段时间间隔来评判。用户的等待延时可以分成两部分:可控等待延时和不可控等待延时。可控等待延时可以理解为能通过技术手段和优化来改进缩短的部分,例如减小图片大小让请求加载更快、减少HTTP请求数等。不可控等待延时则是不能或很难通过前后端技术手段来改进优化的,例如鼠标点击延时、CPU计算时间延时、ISP(Internet Service Provider,互联网服务提供商)网络传输延时等。所以要知道的是,前端中的所有优化都是针对可控等待延时这部分来进行的,下面来了解一下如何获取和评价一个页面的具体性能。

5.4.1 前端性能测试

获取和衡量一个页面的性能,主要可以通过以下几个方面:Performance Timing API、Profile工具、页面埋点计时、资源加载时序图分析。

前端性能与异常上报

2018/08/22 · 基础技术 · 性能

原文出处: counterxing   

一、Performance Timing API

Performance Timing API是一个支持Internet Explorer 9以上版本及WebKit内核浏览器中用于记录页面加载和解析过程中关键时间点的机制,它可以详细记录每个页面资源从开始加载到解析完成这一过程中具体操作发生的时间点,这样根据开始和结束时间戳就可以计算出这个过程所花的时间了。

图1为W3C标准中Performance Timing资源加载和解析过程记录各个关键点的示意图,浏览器中加载和解析一个HTML文件的详细过程先后经历unload、redirect、App Cache、DNS、TCP、Request、Response、Processing、onload几个阶段,每个过程开始和结束的关键时间戳浏览器已经使用performance.timing来记录了,所以根据这个记录并结合简单的计算,我们就可以得到页面中每个过程所消耗的时间。

图片 1

图1 performance API关键时间点记录

function performanceTest() { let timing = performance.timing, readyStart = timing.fetchStart - timing.navigationStart, redirectTime = timing.redirectEnd - timing.redirectStart, appcacheTime = timing.domainLookupStart - timing.fetchStart, unloadEventTime = timing.unloadEventEnd - timing.unloadEventStart, lookupDomainTime = timing.domainLookupEnd - timing.domainLookupStart, connectTime = timing.connectEnd - timing.connectStart, requestTime = timing.responseEnd - timing.requestStart, initDomTreeTime = timing.domInteractive - timing.responseEnd, domReadyTime = timing.domComplete - timing.domInteractive, loadEventTime = timing.loadEventEnd - timing.loadEventStart, loadTime = timing.loadEventEnd - timing.navigationStart; console.log('准备新页面时间耗时: ' + readyStart); console.log('redirect 重定向耗时: ' + redirectTime); console.log('Appcache 耗时: ' + appcacheTime); console.log('unload 前文档耗时: ' + unloadEventTime); console.log('DNS 查询耗时: ' + lookupDomainTime); console.log('TCP连接耗时: ' + connectTime); console.log('request请求耗时: ' + requestTime); console.log('请求完毕至DOM加载: ' + initDomTreeTime); console.log('解析DOM树耗时: ' + domReadyTime); console.log('load事件耗时: ' + loadEventTime); console.log('加载时间耗时: ' + loadTime); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function performanceTest() {
    let timing = performance.timing,
    readyStart = timing.fetchStart - timing.navigationStart,
    redirectTime = timing.redirectEnd - timing.redirectStart,
    appcacheTime = timing.domainLookupStart - timing.fetchStart,
    unloadEventTime = timing.unloadEventEnd - timing.unloadEventStart,
    lookupDomainTime = timing.domainLookupEnd - timing.domainLookupStart,
    connectTime = timing.connectEnd - timing.connectStart,
    requestTime = timing.responseEnd - timing.requestStart,
    initDomTreeTime = timing.domInteractive - timing.responseEnd,
    domReadyTime = timing.domComplete - timing.domInteractive,
    loadEventTime = timing.loadEventEnd - timing.loadEventStart,
    loadTime = timing.loadEventEnd - timing.navigationStart;
    console.log('准备新页面时间耗时: ' + readyStart);
    console.log('redirect 重定向耗时: ' + redirectTime);
    console.log('Appcache 耗时: ' + appcacheTime);
    console.log('unload 前文档耗时: ' + unloadEventTime);
    console.log('DNS 查询耗时: ' + lookupDomainTime);
    console.log('TCP连接耗时: ' + connectTime);
    console.log('request请求耗时: ' + requestTime);
    console.log('请求完毕至DOM加载: ' + initDomTreeTime);
    console.log('解析DOM树耗时: ' + domReadyTime);
    console.log('load事件耗时: ' + loadEventTime);
    console.log('加载时间耗时: ' + loadTime);
}

通过上面的时间戳计算可以得到几个关键步骤所消耗的时间,对前端有意义的几个过程主要是解析DOM树耗时、load事件耗时和整个加载过程耗时等,不过在页面性能获取时我们可以尽量获取更详细的数据信息,以供后面分析。除了资源加载解析的关键点计时,performance还提供了一些其他方面的功能,我们可以根据具体需要进行选择使用。

performance.memory // 内存占用的具体数据 performance.now() // performance.now()方法返回当前网页自performance.timing到现在的时间,可以精确到微秒,用于更加精确的计数。但实际上,目前网页性能通过毫秒来计算就足够了。 performance.getEntries() // 获取页面所有加载资源的performance timing情况。浏览器获取网页时,会对网页中每一个对象(脚本文件、样式表、图片文件等)发出一个HTTP请求。performance.getEntries方法以数组形式返回所有请求的时间统计信息。 performance.navigation // performance还可以提供用户行为信息,例如网络请求的类型和重定向次数等,一般都存放在performance.navigation对象里面。 performance.navigation.redirectCount // 记录当前网页重定向跳转的次数。

1
2
3
4
5
performance.memory // 内存占用的具体数据
performance.now() // performance.now()方法返回当前网页自performance.timing到现在的时间,可以精确到微秒,用于更加精确的计数。但实际上,目前网页性能通过毫秒来计算就足够了。
performance.getEntries() // 获取页面所有加载资源的performance timing情况。浏览器获取网页时,会对网页中每一个对象(脚本文件、样式表、图片文件等)发出一个HTTP请求。performance.getEntries方法以数组形式返回所有请求的时间统计信息。
performance.navigation // performance还可以提供用户行为信息,例如网络请求的类型和重定向次数等,一般都存放在performance.navigation对象里面。
performance.navigation.redirectCount // 记录当前网页重定向跳转的次数。

参考资料:https://www.w3.org/TR/resource-timing/。

概述

对于后台开发来说,记录日志是一种非常常见的开发习惯,通常我们会使用try...catch代码块来主动捕获错误、对于每次接口调用,也会记录下每次接口调用的时间消耗,以便我们监控服务器接口性能,进行问题排查。

刚进公司时,在进行Node.js的接口开发时,我不太习惯每次排查问题都要通过跳板机登上服务器看日志,后来慢慢习惯了这种方式。

举个例子:

JavaScript

/** * 获取列表数据 * @parma req, res */ exports.getList = async function (req, res) { //获取请求参数 const openId = req.session.userinfo.openId; logger.info(`handler getList, user openId is ${openId}`); try { // 拿到列表数据 const startTime = new Date().getTime(); let res = await ListService.getListFromDB(openId); logger.info(`handler getList, ListService.getListFromDB cost time ${new Date().getTime() - startDate}`); // 对数据处理,返回给前端 // ... } catch(error) { logger.error(`handler getList is error, ${JSON.stringify(error)}`); } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取列表数据
* @parma req, res
*/
exports.getList = async function (req, res) {
    //获取请求参数
    const openId = req.session.userinfo.openId;
    logger.info(`handler getList, user openId is ${openId}`);
 
    try {
        // 拿到列表数据
        const startTime = new Date().getTime();
        let res = await ListService.getListFromDB(openId);
        logger.info(`handler getList, ListService.getListFromDB cost time ${new Date().getTime() - startDate}`);
        // 对数据处理,返回给前端
        // ...
    } catch(error) {
        logger.error(`handler getList is error, ${JSON.stringify(error)}`);
    }
};

以下代码经常会出现在用Node.js的接口中,在接口中会统计查询DB所耗时间、亦或是统计RPC服务调用所耗时间,以便监测性能瓶颈,对性能做优化;又或是对异常使用try ... catch主动捕获,以便随时对问题进行回溯、还原问题的场景,进行bug的修复。

而对于前端来说呢?可以看以下的场景。

最近在进行一个需求开发时,偶尔发现webgl渲染影像失败的情况,或者说影像会出现解析失败的情况,我们可能根本不知道哪张影像会解析或渲染失败;又或如最近开发的另外一个需求,我们会做一个关于webgl渲染时间的优化和影像预加载的需求,如果缺乏性能监控,该如何统计所做的渲染优化和影像预加载优化的优化比例,如何证明自己所做的事情具有价值呢?可能是通过测试同学的黑盒测试,对优化前后的时间进行录屏,分析从进入页面到影像渲染完成到底经过了多少帧图像。这样的数据,可能既不准确、又较为片面,设想测试同学并不是真正的用户,也无法还原真实的用户他们所处的网络环境。回过头来发现,我们的项目,虽然在服务端层面做好了日志和性能统计,但在前端对异常的监控和性能的统计。对于前端的性能与异常上报的可行性探索是有必要的。

二、 Profile工具

Performance Timing API描述了页面资源从加载到解析各个阶段的执行关键点时间记录,但是无法统计JavaScript执行过程中系统资源的占用情况。Profile是Chrome和Firefox等标准浏览器提供的一种用于测试页面脚本运行时系统内存和CPU资源占用情况的API,以Chrome浏览器为例,结合Profile,可以实现以下几个功能。

1.分析页面脚本执行过程中最耗资源的操作

2.记录页面脚本执行过程中JavaScript对象消耗的内存与堆栈的使用情况

3.检测页面脚本执行过程中CPU占用情况

使用console.profile()和console.profileEnd()就可以分析中间一段代码执行时系统的内存或CPU资源的消耗情况,然后配合浏览器的Profile查看比较消耗系统内存或CPU资源的操作,这样就可以有针对性地进行优化了。

console.profile(); // TODOS,需要测试的页面逻辑动作 for (let i = 0; i < 100000; i++) { console.log(i * i); } console.profileEnd();

1
2
3
4
5
6
console.profile();
// TODOS,需要测试的页面逻辑动作
for (let i = 0; i < 100000; i++) {
    console.log(i * i);
}
console.profileEnd();

异常捕获

对于前端来说,我们需要的异常捕获无非为以下两种:

  • 接口调用情况;
  • 页面逻辑是否错误,例如,用户进入页面后页面显示白屏;

对于接口调用情况,在前端通常需要上报客户端相关参数,例如:用户OS与浏览器版本、请求参数(如页面ID);而对于页面逻辑是否错误问题,通常除了用户OS与浏览器版本外,需要的是报错的堆栈信息及具体报错位置。

三、 页面埋点计时

使用Profile可以在一定程度上帮助我们分析页面的性能,但缺点是不够灵活。实际项目中,我们不会过多关注页面内存或CPU资源的消耗情况,因为JavaScript有自动内存回收机制。我们关注更多的是页面脚本逻辑执行的时间。除了Performance Timing的关键过程耗时计算,我们还希望检测代码的具体解析或执行时间,这就不能写很多的console.profile()和console.profileEnd()来逐段实现,为了更加简单地处理这种情况,往往选择通过脚本埋点计时的方式来统计每部分代码的运行时间。

页面JavaScript埋点计时比较容易实现,和Performance Timing记录时间戳有点类似,我们可以记录JavaScript代码开始执行的时间戳,后面在需要记录的地方埋点记录结束时的时间戳,最后通过差值来计算一段HTML解析或JavaScript解析执行的时间。为了方便操作,可以将某个操作开始和结束的时间戳记录到一个数组中,然后分析数组之间的间隔就得到每个步骤的执行时间,下面来看一个时间点记录和分析的例子。

let timeList = []; function addTime(tag) { timeList.push({ "tag": tag, "time": +new Date }); } addTime("loading"); timeList.push({ "tag": "load", "time": +new Date() }); // TODOS,load加载时的操作 timeList.push({ "tag": "load", "time": +new Date() }); timeList.push({ "tag": "process", "time": +new Date() }); // TODOS,process处理时的操作 timeList.push({ "tag": "process", "time": +new Date() }); parseTime(timeList); // 输出{load: 时间毫秒数,process: 时间毫秒数} function parseTime(time) { let timeStep = {}, endTime; for (let i = 0, len = time.length; i < len; i++) { if (!time[i]) continue; endTime = {}; for (let j = i + 1; j < len; j++) { if (time[j] && time[i].tag == time[j].tag) { endTime.tag = time[j].tag; endTime.time = time[j].time; time[j] = null; } } if (endTime.time >= 0 && endTime.tag) { timeStep[endTime.tag] = endTime.time - time[i].time; } } return timeStep; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
let timeList = [];
function addTime(tag) {
    timeList.push({
        "tag": tag,
        "time": +new Date
    });
}
addTime("loading");
timeList.push({
    "tag": "load",
    "time": +new Date()
});
// TODOS,load加载时的操作
timeList.push({
    "tag": "load",
    "time": +new Date()
});
timeList.push({
    "tag": "process",
    "time": +new Date()
});
// TODOS,process处理时的操作
timeList.push({
    "tag": "process",
    "time": +new Date()
});
parseTime(timeList); // 输出{load: 时间毫秒数,process: 时间毫秒数}
function parseTime(time) {
    let timeStep = {},
    endTime;
    for (let i = 0, len = time.length; i < len; i++) {
        if (!time[i]) continue;
        endTime = {};
        for (let j = i + 1; j < len; j++) {
            if (time[j] && time[i].tag == time[j].tag) {
                endTime.tag = time[j].tag;
                endTime.time = time[j].time;
                time[j] = null;
            }
        }
        if (endTime.time >= 0 && endTime.tag) {
            timeStep[endTime.tag] = endTime.time - time[i].time;
        }
    }
    return timeStep;
}

这种方式常常在移动端页面中使用,因为移动端浏览器HTML解析和JavaScript执行相对较慢,通常为了进行性能优化,我们需要找到页面中执行JavaScript耗时的操作,如果将关键JavaScript的执行过程进行埋点计时并上报,就可以轻松找出JavaScript执行慢的地方,并有针对性地进行优化。

异常捕获方法

四、资源加载时序图

我们还可以借助浏览器或其他工具的资源加载时序图来帮助分析页面资源加载过程中的性能问题。这种方法可以粗粒度地宏观分析浏览器的所有资源文件请求耗时和文件加载顺序情况,如保证CSS和数据请求等关键性资源优先加载,JavaScript文件和页面中非关键性图片等内容延后加载。如果因为某个资源的加载十分耗时而阻塞了页面的内容展示,那就要着重考虑。所以,我们需要通过资源加载时序图来辅助分析页面上资源加载顺序的问题。

图片 2

图2

图2为使用Fiddler获取浏览器访问地址 http://www.jixianqianduan.com 时的资源加载时序图。根据此图,我们可以很直观地看到页面上各个资源加载过程所需要的时间和先后顺序,有利于找出加载过程中比较耗时的文件资源,帮助我们有针对性地进行优化。

1 赞 2 收藏 评论

图片 3

全局捕获

可以通过全局监听异常来捕获,通过window.onerror或者addEventListener,看以下例子:

JavaScript

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) { console.log('errorMessage: ' + errorMessage); // 异常信息 console.log('scriptURI: ' + scriptURI); // 异常文件路径 console.log('lineNo: ' + lineNo); // 异常行号 console.log('columnNo: ' + columnNo); // 异常列号 console.log('error: ' + error); // 异常堆栈信息 // ... // 异常上报 }; throw new Error('这是一个错误');

1
2
3
4
5
6
7
8
9
10
window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
  console.log('errorMessage: ' + errorMessage); // 异常信息
  console.log('scriptURI: ' + scriptURI); // 异常文件路径
  console.log('lineNo: ' + lineNo); // 异常行号
  console.log('columnNo: ' + columnNo); // 异常列号
  console.log('error: ' + error); // 异常堆栈信息
  // ...
  // 异常上报
};
throw new Error('这是一个错误');

图片 4

通过window.onerror事件,可以得到具体的异常信息、异常文件的URL、异常的行号与列号及异常的堆栈信息,再捕获异常后,统一上报至我们的日志服务器。

亦或是,通过window.addEventListener方法来进行异常上报,道理同理:

JavaScript

window.addEventListener('error', function() { console.log(error); // ... // 异常上报 }); throw new Error('这是一个错误');

1
2
3
4
5
6
window.addEventListener('error', function() {
  console.log(error);
  // ...
  // 异常上报
});
throw new Error('这是一个错误');

图片 5

try… catch

使用try... catch虽然能够较好地进行异常捕获,不至于使得页面由于一处错误挂掉,但try ... catch捕获方式显得过于臃肿,大多代码使用try ... catch包裹,影响代码可读性。

常见问题

跨域脚本无法准确捕获异常

通常情况下,我们会把静态资源,如JavaScript脚本放到专门的静态资源服务器,亦或者CDN,看以下例子:

<!DOCTYPE html> <html> <head> <title></title> </head> <body> <script type="text/javascript"> // 在index.html window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) { console.log('errorMessage: ' + errorMessage); // 异常信息 console.log('scriptURI: ' + scriptURI); // 异常文件路径 console.log('lineNo: ' + lineNo); // 异常行号 console.log('columnNo: ' + columnNo); // 异常列号 console.log('error: ' + error); // 异常堆栈信息 // ... // 异常上报 }; </script> <script src="./error.js"></script> </body> </html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
  <script type="text/javascript">
    // 在index.html
    window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
      console.log('errorMessage: ' + errorMessage); // 异常信息
      console.log('scriptURI: ' + scriptURI); // 异常文件路径
      console.log('lineNo: ' + lineNo); // 异常行号
      console.log('columnNo: ' + columnNo); // 异常列号
      console.log('error: ' + error); // 异常堆栈信息
      // ...
      // 异常上报
    };
 
  </script>
  <script src="./error.js"></script>
</body>
</html>

JavaScript

// error.js throw new Error('这是一个错误');

1
2
// error.js
throw new Error('这是一个错误');

图片 6

结果显示,跨域之后window.onerror根本捕获不到正确的异常信息,而是统一返回一个Script error

解决方案:对script标签增加一个crossorigin=”anonymous”,并且服务器添加Access-Control-Allow-Origin

<script src="http://cdn.xxx.com/index.js" crossorigin="anonymous"></script>

1
<script src="http://cdn.xxx.com/index.js" crossorigin="anonymous"></script>

sourceMap

通常在生产环境下的代码是经过webpack打包后压缩混淆的代码,所以我们可能会遇到这样的问题,如图所示:

图片 7

我们发现所有的报错的代码行数都在第一行了,为什么呢?这是因为在生产环境下,我们的代码被压缩成了一行:

JavaScript

!function(e){var n={};function r(o){if(n[o])return n[o].exports;var t=n[o]={i:o,l:!1,exports:{}};return e[o].call(t.exports,t,t.exports,r),t.l=!0,t.exports}r.m=e,r.c=n,r.d=function(e,n,o){r.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,n){if(1&n&&(e=r(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var t in e)r.d(o,t,function(n){return e[n]}.bind(null,t));return o},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},r.p="",r(r.s=0)}([function(e,n){throw window.onerror=function(e,n,r,o,t){console.log("errorMessage: "+e),console.log("scriptURI: "+n),console.log("lineNo: "+r),console.log("columnNo: "+o),console.log("error: "+t);var l={errorMessage:e||null,scriptURI:n||null,lineNo:r||null,columnNo:o||null,stack:t&&t.stack?t.stack:null};if(XMLHttpRequest){var u=new XMLHttpRequest;u.open("post","/middleware/errorMsg",!0),u.setRequestHeader("Content-Type","application/json"),u.send(JSON.stringify(l))}},new Error("这是一个错误")}]);

1
!function(e){var n={};function r(o){if(n[o])return n[o].exports;var t=n[o]={i:o,l:!1,exports:{}};return e[o].call(t.exports,t,t.exports,r),t.l=!0,t.exports}r.m=e,r.c=n,r.d=function(e,n,o){r.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,n){if(1&n&&(e=r(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var t in e)r.d(o,t,function(n){return e[n]}.bind(null,t));return o},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},r.p="",r(r.s=0)}([function(e,n){throw window.onerror=function(e,n,r,o,t){console.log("errorMessage: "+e),console.log("scriptURI: "+n),console.log("lineNo: "+r),console.log("columnNo: "+o),console.log("error: "+t);var l={errorMessage:e||null,scriptURI:n||null,lineNo:r||null,columnNo:o||null,stack:t&&t.stack?t.stack:null};if(XMLHttpRequest){var u=new XMLHttpRequest;u.open("post","/middleware/errorMsg",!0),u.setRequestHeader("Content-Type","application/json"),u.send(JSON.stringify(l))}},new Error("这是一个错误")}]);

在我的开发过程中也遇到过这个问题,我在开发一个功能组件库的时候,使用npm link了我的组件库,但是由于组件库被npm link后是打包后的生产环境下的代码,所有的报错都定位到了第一行。

解决办法是开启webpacksource-map,我们利用webpack打包后的生成的一份.map的脚本文件就可以让浏览器对错误位置进行追踪了。此处可以参考webpack document

其实就是webpack.config.js中加上一行devtool: 'source-map',如下所示,为示例的webpack.config.js

JavaScript

var path = require('path'); module.exports = { devtool: 'source-map', mode: 'development', entry: './client/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'client') } }

1
2
3
4
5
6
7
8
9
10
var path = require('path');
module.exports = {
    devtool: 'source-map',
    mode: 'development',
    entry: './client/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'client')
    }
}

webpack打包后生成对应的source-map,这样浏览器就能够定位到具体错误的位置:

图片 8

开启source-map的缺陷是兼容性,目前只有Chrome浏览器和Firefox浏览器才对source-map支持。不过我们对这一类情况也有解决办法。可以使用引入npm库来支持source-map,可以参考mozilla/source-map。这个npm库既可以运行在客户端也可以运行在服务端,不过更为推荐的是在服务端使用Node.js对接收到的日志信息时使用source-map解析,以避免源代码的泄露造成风险,如下代码所示:

JavaScript

const express = require('express'); const fs = require('fs'); const router = express.Router(); const sourceMap = require('source-map'); const path = require('path'); const resolve = file => path.resolve(__dirname, file); // 定义post接口 router.get('/error/', async function(req, res) { // 获取前端传过来的报错对象 let error = JSON.parse(req.query.error); let url = error.scriptURI; // 压缩文件路径 if (url) { let fileUrl = url.slice(url.indexOf('client/')) + '.map'; // map文件路径 // 解析sourceMap let consumer = await new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' + fileUrl), 'utf8')); // 返回一个promise对象 // 解析原始报错数据 let result = consumer.originalPositionFor({ line: error.lineNo, // 压缩后的行号 column: error.columnNo // 压缩后的列号 }); console.log(result); } }); module.exports = router;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const express = require('express');
const fs = require('fs');
const router = express.Router();
const sourceMap = require('source-map');
const path = require('path');
const resolve = file => path.resolve(__dirname, file);
// 定义post接口
router.get('/error/', async function(req, res) {
    // 获取前端传过来的报错对象
    let error = JSON.parse(req.query.error);
    let url = error.scriptURI; // 压缩文件路径
    if (url) {
        let fileUrl = url.slice(url.indexOf('client/')) + '.map'; // map文件路径
        // 解析sourceMap
        let consumer = await new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' + fileUrl), 'utf8')); // 返回一个promise对象
        // 解析原始报错数据
        let result = consumer.originalPositionFor({
            line: error.lineNo, // 压缩后的行号
            column: error.columnNo // 压缩后的列号
        });
        console.log(result);
    }
});
module.exports = router;

如下图所示,我们已经可以看到,在服务端已经成功解析出了具体错误的行号、列号,我们可以通过日志的方式进行记录,达到了前端异常监控的目的。

图片 9

Vue捕获异常

在我的项目中就遇到这样的问题,使用了js-tracker这样的插件来统一进行全局的异常捕获和日志上报,结果发现我们根本捕获不到Vue组件的异常,查阅资料得知,在Vue中,异常可能被Vue自身给try ... catch了,不会传到window.onerror事件触发,那么我们如何把Vue组件中的异常作统一捕获呢?

使用Vue.config.errorHandler这样的Vue全局配置,可以在Vue指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和Vue 实例。

JavaScript

Vue.config.errorHandler = function (err, vm, info) { // handle error // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子 // 只在 2.2.0+ 可用 }

1
2
3
4
5
Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  // 只在 2.2.0+ 可用
}

React中,可以使用ErrorBoundary组件包括业务组件的方式进行异常捕获,配合React 16.0+新出的componentDidCatch API,可以实现统一的异常捕获和日志上报。

JavaScript

class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // You can also log the error to an error reporting service logErrorToMyService(error, info); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }
 
  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

使用方式如下:

<ErrorBoundary> <MyWidget /> </ErrorBoundary>

1
2
3
<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

本文由俄罗斯贵宾会发布于Web前端,转载请注明出处:前端性能优化 —— 前端性能分析

您可能还会对下面的文章感兴趣: