函数的性能,10种最常见的Web应用程序的性能问题

HTML imports 入门

2015/02/10 · HTML5 ·
HTML,
imports

本文由 伯乐在线 –
XfLoops
翻译,周进林
校稿。未经许可,禁止转载!
英文出处:webcomponents.org。欢迎加入翻译组。

Template、Shadow
DOM及Custom
Elements 让你创建UI组件比以前更容易了。但是像HTML、CSS、JavaScript这样的资源仍然需要一个个地去加载,这是很没效率的。

删除重复依赖也并不简单。例如,现在加载jQuery
UI或Bootstrap就需要为JavaScript、CSS及Web
Fonts添加单独的标签。如果你的Web
组件应用了多重的依赖,那事情就变得更为复杂。

HTML 导入让你以一个合并的HTML文件来加载这些资源。

测试 JavaScript 函数的性能

2017/08/08 · JavaScript
· 函数,
时间

本文由 伯乐在线 –
Wing
翻译,周进林
校稿。未经许可,禁止转载!
英文出处:Peter
Bengtsson。欢迎加入翻译组。

在软件中,性能一直扮演着重要的角色。在Web应用中,性能变得更加重要,因为如果页面速度很慢的话,用户就会很容易转去访问我们的竞争对手的网站。作为专业的web开发人员,我们必须要考虑这个问题。有很多“古老”的关于性能优化的最佳实践在今天依然可行,例如最小化请求数目,使用CDN以及不编写阻塞页面渲染的代码。然而,随着越来越多的web应用都在使用JavaScript,确保我们的代码运行的很快就变得很重要。

假设你有一个正在工作的函数,但是你怀疑它运行得没有期望的那样快,并且你有一个改善它性能的计划。那怎么去证明这个假设呢?在今天,有什么最佳实践可以用来测试JavaScript函数的性能呢?一般来说,完成这个任务的最佳方式是使用内置的performance.now()函数,来衡量函数运行前和运行后的时间。

在这篇文章中,我们会讨论如何衡量代码运行时间,以及有哪些技术可以避免一些常见的“陷阱”。

10种最常见的Web应用程序的性能问题

2015/04/07 · HTML5,
JavaScript ·
性能

本文由 伯乐在线 –
段昕理
翻译,艾凌风
校稿。未经许可,禁止转载!
英文出处:www.neotys.com。欢迎加入翻译组。

Web应用程序总是不可避免的会发生问题。Neotys公司(法国一家负载测试解决方案提供商)的业务主要是通过网站监控和测试帮助您避免这些问题。但任何地方都可能出问题,有时候你只需要知道去哪找就可以了。因此,我们将您经常碰到的一些性能问题并整理成一个简短的指引。

请务必要记住,解决性能问题的最佳方式就是在其影响用户之前就发现并消除。一个良好的维护计划可以成为你的好帮手。制定停机时间策略,创建冗余和扩展计划。为用户负载在一个月或一年后可能会达到的量级做提前的思考。当然,首先要定期做测试负载并持续监控产品性能。

无论你对网站考虑得如何周全,但是有些问题总是要发生的。下面是一些常见性能问题的原因和解决办法。

使用HTML导入

为加载一个HTML文件,你需要增加一个link标签,其rel属性为import,herf属性是HTML文件的路径。例如,如果你想把component.html加载到index.html:

index.html

XHTML

<link rel=”import” href=”component.html” >

1
<link rel="import" href="component.html" >

你可以往HTML导入文件(译者注:本文将“ the imported
HTML”译为“HTML导入文件”,将“the original
HTML”译为“HTML主文件”。例如,index.html是HTML主文件,component.html是HTML导入文件。)添加任何的资源,包括脚本、样式表及字体,就跟往普通的HTML添加资源一样。

component.html

XHTML

<link rel=”stylesheet” href=”css/style.css”> <script
src=”js/script.js”></script>

1
2
<link rel="stylesheet" href="css/style.css">
<script src="js/script.js"></script>

doctype、html、 head、 body这些标签是不需要的。HTML
导入会立即加载要导入的文档,解析文档中的资源,如果有脚本的话也会立即执行它们。

Performance.now()

高分辨率时间API提供了一个名为now()的函数,它返回一个DOMHighResTimeStamp对象,这是一个浮点数值,以毫秒级别(精确到千分之一毫秒)显示当前时间。单独这个数值并不会为你的分析带来多少价值,但是两个这样的数值的差值,就可以精确描述过去了多少时间。

这个函数除了比内置的Date对象更加精确以外,它还是“单调”的,简单说,这意味着它不会受操作系统(例如,你笔记本上的操作系统)周期性修改系统时间影响。更简单的说,定义两个Date实例,计算它们的差值,并不代表过去了多少时间。

“单调性”的数学定义是“(一个函数或者数值)以从不减少或者从不增加的方式改变”。

我们可以从另外一种途径来解释它,即想象使用它来在一年中让时钟向前或者向后改变。例如,当你所在国家的时钟都同意略过一个小时,以便最大化利用白天的时间。如果你在时钟修改之前创建了一个Date实例,然后在修改之后创建了另外一个,那么查看这两个实例的差值,看上去可能像“1小时零3秒又123毫秒”。而使用两个performance.now()实例,差值会是“3秒又123毫秒456789之一毫秒”。

在这一节中,我不会涉及这个API的过多细节。如果你想学习更多相关知识或查看更多如何使用它的示例,我建议你阅读这篇文章:Discovering
the High Resolution Time
API。

既然你知道高分辨率时间API是什么以及如何使用它,那么让我们继续深入看一下它有哪些潜在的缺点。但是在此之前,我们定义一个名为makeHash()的函数,在这篇文章剩余的部分,我们会使用它。

JavaScript

function makeHash(source) {  var hash = 0;  if (source.length === 0)
return hash;  for (var i = 0; i < source.length; i++) {    var char =
source.charCodeAt(i);    hash = ((hash<<5)-hash)+char;    hash =
hash & hash; // Convert to 32bit integer  }  return hash; }

1
2
3
4
5
6
7
8
9
10
function makeHash(source) {
 var hash = 0;
 if (source.length === 0) return hash;
 for (var i = 0; i < source.length; i++) {
   var char = source.charCodeAt(i);
   hash = ((hash<<5)-hash)+char;
   hash = hash & hash; // Convert to 32bit integer
 }
 return hash;
}

我们可以通过下面的代码来衡量这个函数的执行效率:

JavaScript

var t0 = performance.now(); var result = makeHash(‘Peter’); var t1 =
performance.now(); console.log(‘Took’, (t1 – t0).toFixed(4),
‘milliseconds to generate:’, result);

1
2
3
4
var t0 = performance.now();
var result = makeHash(‘Peter’);
var t1 = performance.now();
console.log(‘Took’, (t1 – t0).toFixed(4), ‘milliseconds to generate:’, result);

如果你在浏览器中运行这些代码,你应该看到类似下面的输出:

JavaScript

Took 0.2730 milliseconds to generate: 77005292

1
Took 0.2730 milliseconds to generate: 77005292

这段代码的在线演示如下所示:

记住这个示例后,让我们开始下面的讨论。

问题 1: 糟糕的代码

糟糕的代码会使Web应用程序出现诸如算法低效、内存溢出、以及死锁等问题。软件版本过旧,或是集成了历史遗留的系统同样会拖累性能。确保你的团队成员都在使用适合其岗位的工具
– 从自动化分析到最佳编程实践的代码审查工具。

执行顺序

浏览器解析HTML文档的方式是线性的,这就是说HTML顶部的script会比底部先执行。并且,浏览器通常会等到JavaScript代码执行完毕后,才会接着解析后面的代码。

为了不让script 妨碍HTML的渲染,你可以在标签中添加async或defer属性(或者你也可以将script 标签放到页面的底部)。defer 属性会延迟脚本的执行,直到全部页面解析完毕。async 属性让浏览器异步地执行脚本,从而不会妨碍HTML的渲染。那么,HTML
导入是怎样工作的呢?

HTML导入文件中的脚本就跟含有defer属性一样。例如在下面的示例中,index.html会先执行script1.js和script2.js
,然后再执行script3.js。

index.html

XHTML

<link rel=”import” href=”component.html”> // 1.
<title>Import Example</title> <script
src=”script3.js”></script> // 4.

1
2
3
<link rel="import" href="component.html"> // 1.
<title>Import Example</title>
<script src="script3.js"></script>        // 4.

component.html

XHTML

<script src=”js/script1.js”></script> // 2. <script
src=”js/script2.js”></script> // 3.

1
2
<script src="js/script1.js"></script>     // 2.
<script src="js/script2.js"></script>     // 3.

1.在index.html 中加载component.html并等待执行

2.执行component.html中的script1.js

3.执行完script1.js后执行component.html中的script2.js

4.执行完 script2.js继而执行index.html中的script3.js

注意,如果给link[rel=”import”]添加async属性,HTML导入会把它当做含有async属性的脚本来对待。它不会等待HTML导入文件的执行和加载,这意味着HTML
导入不会妨碍HTML主文件的渲染。这也给提升网站性能带来了可能,除非有其他的脚本依赖于HTML导入文件的执行。

缺陷1 – 意外衡量不重要的事情

在上面的示例中,你可以注意到,我们在两次调用performance.now()中间只调用了makeHash()函数,然后将它的值赋给result变量。这给我们提供了函数的执行时间,而没有其他的干扰。我们也可以按照下面的方式来衡量代码的效率:

JavaScript

var t0 = performance.now(); console.log(makeHash(‘Peter’));  // bad
idea! var t1 = performance.now(); console.log(‘Took’, (t1 –
t0).toFixed(4), ‘milliseconds’);

1
2
3
4
var t0 = performance.now();
console.log(makeHash(‘Peter’));  // bad idea!
var t1 = performance.now();
console.log(‘Took’, (t1 – t0).toFixed(4), ‘milliseconds’);

这个代码片段的在线演示如下所示:

但是在这种情况下,我们将会测量调用makeHash(‘Peter’)函数花费的时间,以及将结果发送并打印到控制台上花费的时间。我们不知道这两个操作中每个操作具体花费多少时间,
只知道总的时间。而且,发送和打印输出的操作所花费的时间会依赖于所用的浏览器,甚至依赖于当时的上下文。

或许你已经完美的意识到console.log方式是不可以预测的。但是执行多个函数同样是错误的,即使每个函数都不会触发I/O操作。例如:

JavaScript

var t0 = performance.now(); var name = ‘Peter’; var result =
makeHash(name.toLowerCase()).toString(); var t1 = performance.now();
console.log(‘Took’, (t1 – t0).toFixed(4), ‘milliseconds to generate:’,
result);

1
2
3
4
5
var t0 = performance.now();
var name = ‘Peter’;
var result = makeHash(name.toLowerCase()).toString();
var t1 = performance.now();
console.log(‘Took’, (t1 – t0).toFixed(4), ‘milliseconds to generate:’, result);

同样,我们不会知道执行时间是怎么分布的。它会是赋值操作、调用toLowerCase()函数或者toString()函数吗?

问题 2:未经优化的数据库

优化好的数据库可以达到很好的安全级别和处理性能,反之,没有优化的数据库可能会拖垮生产环境下的应用程序。索引的缺失会减慢SQL的查询性能,从而使整个网站变慢。一定要用脚本和文件分析检查任何低效的查询。

跨域导入

从根本上说,HTML导入是不能从其他的域名导入资源的。

比如,你不能从向 
导入HTML
文件。为了绕过这个限制,可以使用CORS(跨域资源共享)。想了解CORS,请看这篇文章。

缺陷 #2 – 只衡量一次

另外一个常见的错误是只衡量一次,然后汇总花费的时间,并以此得出结论。很可能执行不同的次数会得出完全不同的结果。执行时间依赖于很多因素:

  • 编辑器热身的时间(例如,将代码编译成字节码的时间)
  • 主线程可能正忙于其它一些我们没有意识到的事情
  • 你的电脑的CPU可能正忙于一些会拖慢浏览器速度的事情

持续改进的方法是重复执行函数,就像这样:

JavaScript

var t0 = performance.now(); for (var i = 0; i < 10; i++) {
 makeHash(‘Peter’); } var t1 = performance.now(); console.log(‘Took’,
((t1 – t0) / 10).toFixed(4), ‘milliseconds to generate’);

1
2
3
4
5
6
var t0 = performance.now();
for (var i = 0; i < 10; i++) {
 makeHash(‘Peter’);
}
var t1 = performance.now();
console.log(‘Took’, ((t1 – t0) / 10).toFixed(4), ‘milliseconds to generate’);

这个示例的在线演示如下所示:

这种方法的风险在于我们的浏览器的JavaScript引擎可能会使用一些优化措施,这意味着当我们第二次调用函数时,如果输入时相同的,那么JavaScript引擎可能会记住了第一次调用的输出,然后简单的返回这个输出。为了解决这个问题,你可以使用很多不同的输入字符串,而不用重复的使用相同的输入(例如‘Peter’)。显然,使用不同的输入进行测试带来的问题就是我们衡量的函数会花费不同的时间。或许其中一些输入会花费比其它输入更长的执行时间。

问题 3:失控的数据增长

数据系统一般会随时间的推移变慢。制定一项计划来管理和监控数据,因为维持数据的增长对高性能的Web应用不可或缺。首先,找出业务中导致数据增长的主因。然后,研究并制定合适的存储解决方案。留意所有数据库、缓存、以及更复杂存储方案的选项。

HTML导入文件中的window和document对象

前面我提过在导入HTML文件的时候里面的脚本是会被执行的,但这并不意味着HTML导入文件中的标签也会被浏览器渲染。你需要写一些JavaScript代码来帮忙。

当在HTML导入文件中使用JavaScript时,有一点要提防的是,HTML导入文件中的document对象实际上指的是HTML主文件中的document对象。以前面的代码为例,index.html和
 component.html
的document都是指index.html的document对象。怎么才能使用HTML导入文件中的document
呢?借助link中的import 属性。

index.html

XHTML

var link = document.querySelector(‘link[rel=”import”]’);
link.addEventListener(‘load’, function(e) { var importedDoc =
link.import; // importedDoc points to the document under component.html
});

1
2
3
4
5
var link = document.querySelector(‘link[rel="import"]’);
link.addEventListener(‘load’, function(e) {
  var importedDoc = link.import;
  // importedDoc points to the document under component.html
});

为了获取component.html中的document 对象,要使用document.currentScript.ownerDocument.

component.html

XHTML

var mainDoc = document.currentScript.ownerDocument; // mainDoc points to
the document under component.html

1
2
var mainDoc = document.currentScript.ownerDocument;
// mainDoc points to the document under component.html

如果你在用webcomponents.js,那么就用document._currentScript来代替document.currentScript。下划线用于填充currentScript属性,因为并不是所有的浏览器都支持这个属性。

component.html

XHTML

var mainDoc = document._currentScript.ownerDocument; // mainDoc points
to the document under component.html

1
2
var mainDoc = document._currentScript.ownerDocument;
// mainDoc points to the document under component.html

通过在脚本开头添加下面的代码,你就可以轻松地访问component.html中的document对象,而不用管浏览器是不是支持HTML导入。

XHTML

document._currentScript = document._currentScript ||
document.currentScript;

1
document._currentScript = document._currentScript || document.currentScript;

缺陷 #3 – 太依赖平均值

在上一节中,我们学习到的一个很好的实践是重复执行一些操作,理想情况下使用不同的输入。然而,我们要记住使用不同的输入带来的问题,即某些输入的执行时间可能会花费所有其它输入的执行时间都长。这样让我们退一步来使用相同的输入。假设我们发送同样的输入十次,每次都打印花费了多长时间。我们会得到像这样的输出:

JavaScript

Took 0.2730 milliseconds to generate: 77005292 Took 0.0234 milliseconds
to generate: 77005292 Took 0.0200 milliseconds to generate: 77005292
Took 0.0281 milliseconds to generate: 77005292 Took 0.0162 milliseconds
to generate: 77005292 Took 0.0245 milliseconds to generate: 77005292
Took 0.0677 milliseconds to generate: 77005292 Took 0.0289 milliseconds
to generate: 77005292 Took 0.0240 milliseconds to generate: 77005292
Took 0.0311 milliseconds to generate: 77005292

1
2
3
4
5
6
7
8
9
10
Took 0.2730 milliseconds to generate: 77005292
Took 0.0234 milliseconds to generate: 77005292
Took 0.0200 milliseconds to generate: 77005292
Took 0.0281 milliseconds to generate: 77005292
Took 0.0162 milliseconds to generate: 77005292
Took 0.0245 milliseconds to generate: 77005292
Took 0.0677 milliseconds to generate: 77005292
Took 0.0289 milliseconds to generate: 77005292
Took 0.0240 milliseconds to generate: 77005292
Took 0.0311 milliseconds to generate: 77005292

请注意第一次时间和其它九次的时间完全不一样。这很可能是因为浏览器中的JavaScript引擎使用了优化措施,需要一些热身时间。我们基本上没有办法避免这种情况,但是会有一些好的补救措施来阻止我们得出一些错误的结论。

一种方式是去计算后面9次的平均时间。另外一种更加使用的方式是收集所有的结果,然后计算“中位数”。基本上,它会将所有的结果排列起来,对结果进行排序,然后取中间的一个值。这是performance.now()函数如此有用的地方,因为无论你做什么,你都可以得到一个数值。

让我们再试一次,这次我们使用中位数函数:

JavaScript

var numbers = []; for (var i=0; i < 10; i++) {  var t0 =
performance.now();  makeHash(‘Peter’);  var t1 = performance.now();
 numbers.push(t1 – t0); } function median(sequence) {  sequence.sort();
 // note that direction doesn’t matter  return
sequence[Math.ceil(sequence.length / 2)]; } console.log(‘Median time’,
median(numbers).toFixed(4), ‘milliseconds’);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var numbers = [];
for (var i=0; i < 10; i++) {
 var t0 = performance.now();
 makeHash(‘Peter’);
 var t1 = performance.now();
 numbers.push(t1 – t0);
}
 
function median(sequence) {
 sequence.sort();  // note that direction doesn’t matter
 return sequence[Math.ceil(sequence.length / 2)];
}
 
console.log(‘Median time’, median(numbers).toFixed(4), ‘milliseconds’);

问题 4:流量峰值

我们通常认为流量的增长是件好事。但是当做完市场推广或是经历了疯狂传播的热门视频后,应用程序如果没有做好相应的准备,任何人都知道流量峰值会造成什么结果。提前准备是关键,同时搭建一个通过模拟用户做监测的预警系统例如 NeoSense。这样一来,你就会提前发现流量增长影响到了业务,从而避免了用户的糟糕体验。

发表评论

电子邮件地址不会被公开。 必填项已用*标注