>  기사  >  웹 프론트엔드  >  고성능 자바스크립트 작성(번역)_자바스크립트 팁

고성능 자바스크립트 작성(번역)_자바스크립트 팁

WBOY
WBOY원래의
2016-05-16 16:39:461175검색

번역자 주: 외국어 번역이 처음이라 언어가 다소 모호할 수 있지만, 과도한 다듬기 없이 작가의 본래 의도를 표현하려고 노력합니다. 또한, 이 글은 내용이 길고 내용이 많아 이해하기 어려울 수 있으므로 자세한 내용을 논의하려면 메시지를 남겨주세요. 이 기사는 주로 V8의 성능 최적화에 중점을 두고 있으며 일부 내용은 모든 JS 엔진에 적용되지 않습니다. 마지막으로 재인쇄시 출처를 꼭 밝혀주세요: )

========================번역 구분선==================== == =======

Google의 V8 엔진(Chrome 및 Node에서 사용)과 같은 많은 JavaScript 엔진은 빠른 실행이 필요한 대규모 JavaScript 애플리케이션을 위해 특별히 설계되었습니다. 개발자이고 메모리 사용량과 페이지 성능이 걱정된다면 사용자 브라우저의 JavaScript 엔진이 어떻게 작동하는지 이해해야 합니다. V8, SpiderMonkey(Firefox), Carakan(Opera), Chakra(IE) 또는 기타 엔진이든 이를 수행하면 앱을 더욱 효과적으로 최적화하는 데 도움이 될 수 있습니다. 이는 특정 브라우저나 엔진에 맞게 최적화해야 한다는 의미는 아닙니다. 그렇게 하지 마세요. 그러나 몇 가지 질문을 스스로에게 물어봐야 합니다. 내 코드에서 코드를 더 효율적으로 만들 수 있나요? 주류 JavaScript 엔진에서는 어떤 최적화가 이루어졌나요

엔진이 최적화할 수 없는 것은 무엇인가요? 가비지 수집기(GC)가 예상한 대로 복구할 수 있나요?

로드 중

고성능 자바스크립트 작성(번역)_자바스크립트 팁빠른 웹사이트는 빠른 스포츠카와 같아서 특별히 맞춤화된 부품이 필요합니다. 이미지 출처:

.

고성능 코드를 작성할 때 흔히 저지르는 몇 가지 함정이 있습니다. 이 글에서는 입증되고 더 나은 코드 작성 방법을 보여 드리겠습니다. 그럼 V8에서는 JavaScript가 어떻게 작동하나요?

JS 엔진에 대한 깊은 이해가 없어도 대규모 웹 애플리케이션 개발에는 문제가 없습니다. 마치 운전을 할 줄 아는 사람이 후드만 보고 엔진 내부를 보지 못하는 것과 같습니다. 자동차 후드. 제가 선택한 브라우저는 Chrome이므로 Chrome의 JavaScript 엔진에 대해 이야기해 보겠습니다. V8은 다음과 같은 핵심 부품으로 구성됩니다.

바이트코드를 실행하거나 단순히 해석하는 것이 아니라 코드가 실행되기 전에 JavaScript 코드를 구문 분석하고 네이티브 기계어 코드를 생성하는 기본 컴파일러입니다. 이 코드는 처음부터 고도로 최적화되어 있지 않습니다.

V8은

객체 모델
    에 객체를 구축합니다. JavaScript에서 개체는 연관 배열로 표시되지만 V8에서는 개체가 쿼리 최적화를 위한 내부 유형 시스템인 숨겨진 클래스로 처리됩니다.
  • 런타임 분석기
  • 는 실행 중인 시스템을 모니터링하고 "핫" 기능(예: 실행하는 데 오랜 시간이 걸리는 코드)을 식별합니다.
  • 컴파일러 최적화
  • 런타임 분석기에 의해 "핫"으로 식별된 코드를 재컴파일 및 최적화하고 "인라인"(예: 함수 호출을 호출 수신자의 본문으로 대체)과 같은 최적화를 수행합니다. ).
  • V8은 역최적화
  • 를 지원합니다. 즉, 최적화 컴파일러가 코드 최적화에 대한 가정이 너무 낙관적이라고 판단하면 최적화된 코드를 삭제합니다.
  • V8에는 가비지 수집기
  • 가 있으며 이것이 어떻게 작동하는지 이해하는 것은 JavaScript를 최적화하는 것만큼 중요합니다.
  • 쓰레기 수거
  • 가비지 컬렉션은
  • 메모리 관리의 한 형태
실제로 더 이상 사용되지 않는 객체가 차지하는 메모리를 회수하려는 컬렉터의 개념입니다. JavaScript와 같은 가비지 수집 언어에서는 애플리케이션에서 계속 참조되는 개체가 지워지지 않습니다.

대부분의 경우 객체 참조를 수동으로 제거할 필요는 없습니다. 필요한 곳에 변수를 배치하기만 하면(이상적으로는 가능한 한 로컬 범위, 즉 함수 외부가 아니라 사용되는 함수 내부) 모든 것이 잘 작동합니다.

고성능 자바스크립트 작성(번역)_자바스크립트 팁

가비지 수집기가 메모리 회수를 시도합니다. 이미지 출처: Valtteri Mäki.

JavaScript에서는 가비지 수집을 강제하는 것이 불가능합니다. 가비지 수집 프로세스는 정리하기에 가장 좋은 시기를 아는 런타임에 의해 제어되므로 이 작업을 수행해서는 안 됩니다.

인용말소에 대한 오해

인터넷에서 JavaScript 메모리 재활용에 관해 delete 키워드에 대해 많은 논의가 있습니다. 객체(맵)의 속성(키)을 삭제하는 데 사용할 수 있지만 일부 개발자는 "참조 제거"를 강제하는 데 사용할 수 있다고 생각합니다. . 가능하면 삭제 사용을 피하는 것이 좋습니다. 다음 예에서는 delete o.x 的弊大于利,因为它改变了o的隐藏类,并使它成为一个"慢对象"。

var o = { x: 1 }; 
delete o.x; // true 
o.x; // undefined

인기 있는 JS 라이브러리에서 인용 제거 기능을 쉽게 찾을 수 있습니다. 이는 언어 목적에 따른 것입니다. 여기서 주목해야 할 것은 런타임 시 "핫" 객체의 구조를 수정하지 않는다는 것입니다. JavaScript 엔진은 이러한 "핫" 개체를 감지하고 최적화를 시도할 수 있습니다. 수명 주기 동안 개체의 구조가 크게 변경되지 않으면 엔진이 개체를 최적화하기가 더 쉬울 것이며 삭제 작업은 실제로 엔진 최적화에 도움이 되지 않는 대규모 구조 변경을 트리거하게 됩니다.

null의 작동 방식에 대한 오해도 있습니다. 개체 참조를 null로 설정한다고 해서 개체가 "null"이 되는 것은 아니며 참조가 null로 설정될 뿐입니다. o.x=null을 사용하는 것이 delete를 사용하는 것보다 낫지만 꼭 필요한 것은 아닙니다.

var o = { x: 1 }; 
o = null;
o; // null
o.x // TypeError

이 참조가 현재 개체에 대한 마지막 참조인 경우 해당 개체는 가비지 수집됩니다. 이 참조가 현재 개체에 대한 마지막 참조가 아닌 경우 개체에 액세스할 수 있으며 가비지 수집되지 않습니다.

또 한 가지 주목해야 할 점은 페이지 수명 주기 동안 전역 변수가 가비지 수집기에 의해 정리되지 않는다는 것입니다. 페이지가 열려 있는 시간에 관계없이 전역 개체 범위의 변수는 JavaScript가 실행될 때 항상 존재합니다.

var myGlobalNamespace = {};

전체 개체는 페이지를 새로 고치거나, 다른 페이지로 이동하거나, 탭을 닫거나, 브라우저를 종료할 때만 정리됩니다. 함수 범위의 변수는 범위를 벗어날 때 지워집니다. 즉, 함수를 종료할 때 더 이상 참조가 없으며 해당 변수는 지워집니다.

경험 법칙

가비지 컬렉터가 최대한 많은 객체를 최대한 빨리 수집하기 위해서는 더 이상 사용하지 않는 객체를 보관하지 마세요. 기억해야 할 몇 가지 사항은 다음과 같습니다.

  • 앞서 언급했듯이 적절한 범위에서 변수를 사용하는 것이 수동으로 역참조하는 것보다 더 나은 대안입니다. 즉, 변수가 함수 범위에서만 사용되는 경우 전역 범위에서 선언하지 마세요. 이는 더 깨끗하고 번거롭지 않은 코드를 의미합니다.
  • 더 이상 필요하지 않은 이벤트 리스너, 특히 곧 소멸될 DOM 객체에 바인딩된 이벤트 리스너를 바인딩 해제하세요.
  • 사용된 데이터가 로컬로 캐시된 경우 캐시를 지우거나 에이징 메커니즘을 사용하여 재사용되지 않은 데이터가 대량으로 저장되지 않도록 하세요.

기능

다음으로 기능에 대해 이야기해보겠습니다. 앞서 말했듯이 가비지 수집은 더 이상 액세스할 수 없는 메모리 블록(객체)을 회수하여 작동합니다. 이를 더 잘 설명하기 위해 몇 가지 예를 들어 보겠습니다.

function foo() {
 var bar = new LargeObject();
 bar.someCall();
}

foo가 반환되면 bar가 가리키는 객체는 더 이상 기존 참조가 없기 때문에 가비지 수집기에 의해 자동으로 재활용됩니다.

비교:

function foo() {
 var bar = new LargeObject();
 bar.someCall();
 return bar;
}

// somewhere else
var b = foo();

이제 bar 객체를 가리키는 참조가 있으므로 bar 객체의 수명 주기는 foo 호출부터 호출자가 다른 변수 b를 지정할 때까지(또는 b가 범위를 벗어날 때까지) 지속됩니다.

폐쇄

내부 함수를 반환하는 함수가 표시되면 해당 내부 함수는 외부 함수가 실행된 후에도 범위 외부 액세스 권한을 얻습니다. 이는 기본 클로저 — 특정 컨텍스트에서 변수를 설정할 수 있는 표현식입니다. 예:

function sum (x) {
 function sumIt(y) {
  return x + y;
 };
 return sumIt;
}

// Usage
var sumA = sum(4);
var sumB = sumA(3);
console.log(sumB); // Returns 7

sum 호출 컨텍스트에서 생성된 함수 개체(sumIt)는 재활용할 수 없으며 전역 변수(sumA)에서 참조되며 sumA(n)을 통해 호출할 수 있습니다.

여기에서 변수 LargeStr에 액세스할 수 있는 또 다른 예를 살펴보겠습니다.

var a = function () {
 var largeStr = new Array(1000000).join('x');
 return function () {
  return largeStr;
 };
}();

예, a()를 통해 LargeStr에 액세스할 수 있으므로 재활용되지 않습니다. 아래의 것은 어떻습니까?

var a = function () {
 var smallStr = 'x';
 var largeStr = new Array(1000000).join('x');
 return function (n) {
  return smallStr;
 };
}();

largeStr에 더 이상 액세스할 수 없으며 이미 가비지 수집 후보입니다. [번역자 주: LargeStr에는 더 이상 외부 참조가 없기 때문에]

定时器

最糟的内存泄漏地方之一是在循环中,或者在setTimeout()/ setInterval()中,但这是相当常见的。思考下面的例子:

var myObj = {
 callMeMaybe: function () {
  var myRef = this;
  var val = setTimeout(function () { 
   console.log('Time is running out!'); 
   myRef.callMeMaybe();
  }, 1000);
 }
};

如果我们运行myObj.callMeMaybe();来启动定时器,可以看到控制台每秒打印出“Time is running out!”。如果接着运行myObj = null,定时器依旧处于激活状态。为了能够持续执行,闭包将myObj传递给setTimeout,这样myObj是无法被回收的。相反,它引用到myObj的因为它捕获了myRef。这跟我们为了保持引用将闭包传给其他的函数是一样的。

同样值得牢记的是,setTimeout/setInterval调用(如函数)中的引用,将需要执行和完成,才可以被垃圾收集。

当心性能陷阱

永远不要优化代码,直到你真正需要。现在经常可以看到一些基准测试,显示N比M在V8中更为优化,但是在模块代码或应用中测试一下会发现,这些优化真正的效果比你期望的要小的多。

고성능 자바스크립트 작성(번역)_자바스크립트 팁

做的过多还不如什么都不做. 图片来源: Tim Sheerman-Chase.

比如我们想要创建这样一个模块:

  • 需要一个本地的数据源包含数字ID
  • 绘制包含这些数据的表格
  • 添加事件处理程序,当用户点击的任何单元格时切换单元格的css class

这个问题有几个不同的因素,虽然也很容易解决。我们如何存储数据,如何高效地绘制表格并且append到DOM中,如何更优地处理表格事件?

面对这些问题最开始(天真)的做法是使用对象存储数据并放入数组中,使用jQuery遍历数据绘制表格并append到DOM中,最后使用事件绑定我们期望地点击行为。

注意:这不是你应该做的

var moduleA = function () {

 return {

  data: dataArrayObject,

  init: function () {
   this.addTable();
   this.addEvents();
  },

  addTable: function () {

   for (var i = 0; i < rows; i++) {
    $tr = $('<tr></tr>');
    for (var j = 0; j < this.data.length; j++) {
     $tr.append('<td>' + this.data[j]['id'] + '</td>');
    }
    $tr.appendTo($tbody);
   }

  },
  addEvents: function () {
   $('table td').on('click', function () {
    $(this).toggleClass('active');
   });
  }

 };
}();

这段代码简单有效地完成了任务。

但在这种情况下,我们遍历的数据只是本应该简单地存放在数组中的数字型属性ID。有趣的是,直接使用DocumentFragment和本地DOM方法比使用jQuery(以这种方式)来生成表格是更优的选择,当然,事件代理比单独绑定每个td具有更高的性能。

要注意虽然jQuery在内部使用DocumentFragment,但是在我们的例子中,代码在循环内调用append并且这些调用涉及到一些其他的小知识,因此在这里起到的优化作用不大。希望这不会是一个痛点,但请务必进行基准测试,以确保自己代码ok。

对于我们的例子,上述的做法带来了(期望的)性能提升。事件代理对简单的绑定是一种改进,可选的DocumentFragment也起到了助推作用。

var moduleD = function () {

 return {

  data: dataArray,

  init: function () {
   this.addTable();
   this.addEvents();
  },
  addTable: function () {
   var td, tr;
   var frag = document.createDocumentFragment();
   var frag2 = document.createDocumentFragment();

   for (var i = 0; i < rows; i++) {
    tr = document.createElement('tr');
    for (var j = 0; j < this.data.length; j++) {
     td = document.createElement('td');
     td.appendChild(document.createTextNode(this.data[j]));

     frag2.appendChild(td);
    }
    tr.appendChild(frag2);
    frag.appendChild(tr);
   }
   tbody.appendChild(frag);
  },
  addEvents: function () {
   $('table').on('click', 'td', function () {
    $(this).toggleClass('active');
   });
  }

 };

}();

接下来看看其他提升性能的方式。你也许曾经在哪读到过使用原型模式比模块模式更优,或听说过使用JS模版框架性能更好。有时的确如此,不过使用它们其实是为了代码更具可读性。对了,还有预编译!让我们看看在实践中表现的如何?

moduleG = function () {};

moduleG.prototype.data = dataArray;
moduleG.prototype.init = function () {
 this.addTable();
 this.addEvents();
};
moduleG.prototype.addTable = function () {
 var template = _.template($('#template').text());
 var html = template({'data' : this.data});
 $tbody.append(html);
};
moduleG.prototype.addEvents = function () {
 $('table').on('click', 'td', function () {
  $(this).toggleClass('active');
 });
};

var modG = new moduleG();

事实证明,在这种情况下的带来的性能提升可以忽略不计。模板和原型的选择并没有真正提供更多的东西。也就是说,性能并不是开发者使用它们的原因,给代码带来的可读性、继承模型和可维护性才是真正的原因。

更复杂的问题包括高效地在canvas上绘制图片和操作带或不带类型数组的像素数据。

在将一些方法用在你自己的应用之前,一定要多了解这些方案的基准测试。也许有人还记得JS模版的shoot-off随后的扩展版。你要搞清楚基准测试不是存在于你看不到的那些虚拟应用,而是应该在你的实际代码中去测试带来的优化。

V8优化技巧

详细介绍了每个V8引擎的优化点在本文讨论范围之外,当然这里也有许多值得一提的技巧。记住这些技巧你就能减少那些性能低下的代码了。

  • 特定模式可以使V8摆脱优化的困境,比如说try-catch。欲了解更多有关哪些函数能或不能进行优化,你可以在V8的脚本工具d8中使用–trace-opt file.js命令。
  • 如果你关心速度,尽量使你的函数职责单一,即确保变量(包括属性,数组,函数参数)只使用相同隐藏类包含的对象。举个例子,别这么干:
    function add(x, y) { 
     return x+y;
    } 
    
    add(1, 2); 
    add('a','b'); 
    add(my_custom_object, undefined);
  • 不要加载未初始化或已删除的元素。如果这么做也不会出现什么错误,但是这样会使速度变慢。
  • 不要使函数体过大,这样会使得优化更加困难。

更多内容可以去看Daniel Clifford在Google I/O的分享 Breaking the JavaScript Speed Limit with V8Optimizing For V8 — A Series也非常值得一读。

对象VS数组:我应该用哪个?

  • 如果你想存储一串数字,或者一些相同类型的对象,使用一个数组。
  • 如果你语义上需要的是一堆的对象的属性(不同类型的),使用一个对象和属性。这在内存方面非常高效,速度也相当快。
  • 整数索引的元素,无论存储在一个数组或对象中,都要比遍历对象的属性快得多
  • 对象的属性比较复杂:它们可以被setter们创建,具有不同的枚举性和可写性。数组中则不具有如此的定制性,而只存在有和无这两种状态。在引擎层面,这允许更多存储结构方面的优化。特别是当数组中存在数字时,例如当你需要容器时,不用定义具有x,y,z属性的类,而只用数组就可以了。

JavaScript中对象和数组之间只有一个的主要区别,那就是数组神奇的length属性。如果你自己来维护这个属性,那么V8中对象和数组的速度是一样快的。

使用对象时的技巧

  • 使用一个构造函数来创建对象。这将确保它创建的所有对象具有相同的隐藏类,并有助于避免更改这些类。作为一个额外的好处,它也略快于Object.create()
  • 你的应用中,对于使用不同类型的对象和其复杂度(在合理的范围内:长原型链往往是有害的,呈现只有一个极少数属性的对象比大对象会快一点)是有没限制的。对于“hot”对象,尽量保持短原型链,并且少属性。

对象克隆

对于应用程序开发人员,对象克隆是一个常见的问题。虽然各种基准测试可以证明V8对这个问题处理得很好,但仍要小心。复制大的东西通常是较慢的——不要这么做。JS中的for..in循环尤其糟糕,因为它有着恶魔般的规范,并且无论是在哪个引擎中,都可能永远不会比任何对象快。

当你一定要在关键性能代码路径上复制对象时,使用数组或一个自定义的“拷贝构造函数”功能明确地复制每个属性。这可能是最快的方式:

function clone(original) {
 this.foo = original.foo;
 this.bar = original.bar;
}
var copy = new clone(original);

模块模式中缓存函数

使用模块模式时缓存函数,可能会导致性能方面的提升。参阅下面的例子,因为它总是创建成员函数的新副本,你看到的变化可能会比较慢。

另外请注意,使用这种方法明显更优,不仅仅是依靠原型模式(经过jsPerf测试确认)。

고성능 자바스크립트 작성(번역)_자바스크립트 팁

使用模块模式或原型模式时的性能提升

这是一个原型模式与模块模式的性能对比测试

 // Prototypal pattern
 Klass1 = function () {}
 Klass1.prototype.foo = function () {
  log('foo');
 }
 Klass1.prototype.bar = function () {
  log('bar');
 }

 // Module pattern
 Klass2 = function () {
  var foo = function () {
   log('foo');
  },
  bar = function () {
   log('bar');
  };

  return {
   foo: foo,
   bar: bar
  }
 }

 // Module pattern with cached functions
 var FooFunction = function () {
  log('foo');
 };
 var BarFunction = function () {
  log('bar');
 };

 Klass3 = function () {
  return {
   foo: FooFunction,
   bar: BarFunction
  }
 }

 // Iteration tests

 // Prototypal
 var i = 1000,
  objs = [];
 while (i--) {
  var o = new Klass1()
  objs.push(new Klass1());
  o.bar;
  o.foo;
 }

 // Module pattern
 var i = 1000,
  objs = [];
 while (i--) {
  var o = Klass2()
  objs.push(Klass2());
  o.bar;
  o.foo;
 }

 // Module pattern with cached functions
 var i = 1000,
  objs = [];
 while (i--) {
  var o = Klass3()
  objs.push(Klass3());
  o.bar;
  o.foo;
 }
// See the test for full details

使用数组时的技巧

接下来说说数组相关的技巧。在一般情况下,不要删除数组元素,这样将使数组过渡到较慢的内部表示。当索引变得稀疏,V8将会使元素转为更慢的字典模式。

数组字面量

数组字面量非常有用,它可以暗示VM数组的大小和类型。它通常用在体积不大的数组中。

// Here V8 can see that you want a 4-element array containing numbers:
var a = [1, 2, 3, 4];

// Don't do this:
a = []; // Here V8 knows nothing about the array
for(var i = 1; i <= 4; i++) {
  a.push(i);
}

存储单一类型VS多类型

将混合类型(比如数字、字符串、undefined、true/false)的数据存在数组中绝不是一个好想法。例如var arr = [1, “1”, undefined, true, “true”]

类型推断的性能测试

正如我们所看到的结果,整数的数组是最快的。

稀疏数组与满数组

当你使用稀疏数组时,要注意访问元素将远远慢于满数组。因为V8不会分配一整块空间给只用到部分空间的数组。取而代之的是,它被管理在字典中,既节约了空间,但花费访问的时间。

稀疏数组与满数组的测试

预分配空间VS动态分配

不要预分配大数组(如大于64K的元素),其最大的大小,而应该动态分配。在我们这篇文章的性能测试之前,请记住这只适用部分JavaScript引擎。

고성능 자바스크립트 작성(번역)_자바스크립트 팁

空字面量与预分配数组在不同的浏览器进行测试

Nitro (Safari)对预分配的数组更有利。而在其他引擎(V8,SpiderMonkey)中,预先分配并不是高效的。

预分配数组测试

// Empty array
var arr = [];
for (var i = 0; i < 1000000; i++) {
 arr[i] = i;
}

// Pre-allocated array
var arr = new Array(1000000);
for (var i = 0; i < 1000000; i++) {
 arr[i] = i;
}

优化你的应用

在Web应用的世界中,速度就是一切。没有用户希望用一个要花几秒钟计算某列总数或花几分钟汇总信息的表格应用。这是为什么你要在代码中压榨每一点性能的重要原因。

고성능 자바스크립트 작성(번역)_자바스크립트 팁

图片来源: Per Olof Forsberg.

理解和提高应用程序的性能是非常有用的同时,它也是困难的。我们推荐以下的步骤来解决性能的痛点:

  • 测量:在您的应用程序中找到慢的地方(约45%)
  • 理解:找出实际的问题是什么(约45%)
  • 修复它! (约10%)

下面推荐的一些工具和技术可以协助你。

基准化(BENCHMARKING)

有很多方式来运行JavaScript代码片段的基准测试其性能——一般的假设是,基准简单地比较两个时间戳。这中模式被jsPerf团队指出,并在SunSpiderKraken的基准套件中使用:

var totalTime,
 start = new Date,
 iterations = 1000;
while (iterations--) {
 // Code snippet goes here
}
// totalTime → the number of milliseconds taken 
// to execute the code snippet 1000 times
totalTime = new Date - start;

在这里,要测试的代码被放置在一个循环中,并运行一个设定的次数(例如6次)。在此之后,开始日期减去结束日期,就得出在循环中执行操作所花费的时间。

然而,这种基准测试做的事情过于简单了,特别是如果你想运行在多个浏览器和环境的基准。垃圾收集器本身对结果是有一定影响的。即使你使用window.고성능 자바스크립트 작성(번역)_자바스크립트 팁这样的解决方案,也必须考虑到这些缺陷。

不管你是否只运行基准部分的代码,编写一个测试套件或编码基准库,JavaScript基准其实比你想象的更多。如需更详细的指南基准,我强烈建议你阅读由Mathias Bynens和John-David Dalton提供的Javascript基准测试

分析(PROFILING)

Chrome开发者工具为JavaScript分析有很好的支持。可以使用此功能检测哪些函数占用了大部分时间,这样你就可以去优化它们。这很重要,即使是代码很小的改变会对整体表现产生重要的影响。

고성능 자바스크립트 작성(번역)_자바스크립트 팁

Chrome 개발자 도구 분석 패널

분석 프로세스는 코드 성능 기준을 얻는 것부터 시작되며, 이는 타임라인 형태로 반영됩니다. 이는 코드가 실행되는 데 걸리는 시간을 알려줍니다. "프로필" 탭에서는 애플리케이션에서 진행 중인 작업을 더 잘 볼 수 있습니다. JavaScript CPU 프로필은 코드에서 사용되는 CPU 시간을 보여주고, CSS 선택기 프로필은 선택기를 처리하는 데 소요되는 시간을 보여주며, 힙 스냅샷은 개체에 사용되는 메모리의 양을 보여줍니다.

이러한 도구를 사용하면 기능 또는 운영 성능 최적화가 실제로 영향을 미치는지 여부를 분리, 조정 및 재분석할 수 있습니다.

고성능 자바스크립트 작성(번역)_자바스크립트 팁2

'프로필' 탭에는 코드 성능 정보가 표시됩니다.

프로파일링에 대한 좋은 소개를 보려면 Zack Grossbart의 Chrome 개발자 도구를 사용한 JavaScript 프로파일링을 읽어보세요.

팁: 이상적으로는 설치된 앱이나 확장 프로그램으로 인해 분석이 어떤 방식으로든 영향을 받지 않도록 하려면 --user-data-dir <empty_directory></empty_directory> 플래그를 사용하여 Chrome을 실행하세요. 대부분의 경우 테스트를 최적화하는 이 방법으로 충분하지만 시간이 더 많이 필요합니다. V8 로고가 도움이 될 수 있는 곳입니다.

메모리 누수 방지 - 3가지 스냅샷 기술

Google 내에서 Chrome 개발자 도구는 Gmail과 같은 팀에서 메모리 누수를 찾아 문제를 해결하는 데 많이 사용됩니다.

고성능 자바스크립트 작성(번역)_자바스크립트 팁

Chrome 개발자 도구의 메모리 통계

메모리 통계에는 개인 메모리 사용량, JavaScript 힙 크기, DOM 노드 수, 저장소 정리, 이벤트 청취 카운터 및 우리 팀이 관심을 갖고 있는 가비지 수집기에 의해 재활용될 항목이 포함됩니다. 로레나 리의 "3스냅샷" 기법 을 읽어보시길 추천드립니다. 이 기술의 요점은 애플리케이션의 일부 동작을 기록하고, 가비지 수집을 강제하고, DOM 노드 수가 예상 기준으로 돌아왔는지 확인한 다음, 3개의 힙 스냅샷을 분석하여 메모리 누수가 있는지 확인하는 것입니다.

단일 페이지 애플리케이션을 위한 메모리 관리

단일 페이지 애플리케이션(예: AngularJS, Backbone, Ember)에서는 메모리 관리가 매우 중요하며 페이지를 새로 고치는 일이 거의 없습니다. 이는 메모리 누수가 매우 명백할 수 있음을 의미합니다. 모바일 단말기의 단일 페이지 애플리케이션에는 장치의 메모리가 제한되어 있고 이메일 클라이언트나 소셜 네트워크와 같이 오래 실행되는 애플리케이션이 있기 때문에 함정이 많습니다. 능력이 클수록 책임감도 커집니다.

이 문제를 해결하는 방법은 여러 가지가 있습니다. Backbone에서는 dispose()를 사용하여 이전 뷰와 참조를 삭제해야 합니다(현재 Backbone(Edge)에서 사용 가능). 이 함수는 최근에 추가되었으며 뷰의 "이벤트" 객체에 추가된 핸들러와 뷰의 세 번째 인수(콜백 컨텍스트)에 전달된 모델 또는 컬렉션에 대한 이벤트 리스너를 제거합니다. dispose()는 뷰의 Remove()에 의해 호출되며 요소가 제거될 때 주요 정리 작업을 처리합니다. Ember와 같은 다른 라이브러리는 메모리 누수를 방지하기 위해 요소가 제거되었음을 감지하면 리스너를 정리합니다.

Derick Bailey의 현명한 조언:

与其了解事件与引用是如何工作的,不如遵循的标准规则来管理JavaScript中的内存。如果你想加载数据到的一个存满用户对象的Backbone集合中,你要清空这个集合使它不再占用内存,那必须这个集合的所有引用以及集合内对象的引用。一旦清楚了所用的引用,资源就会被回收。这就是标准的JavaScript垃圾回收规则。

在文章中,Derick涵盖了许多使用Backbone.js时的常见内存缺陷,以及如何解决这些问题。

Felix Geisendörfer的在Node中调试内存泄漏的教程也值得一读,尤其是当它形成了更广泛SPA堆栈的一部分。

减少回流(REFLOWS)

当浏览器重新渲染文档中的元素时需要 重新计算它们的位置和几何形状,我们称之为回流。回流会阻塞用户在浏览器中的操作,因此理解提升回流时间是非常有帮助的。

고성능 자바스크립트 작성(번역)_자바스크립트 팁

回流时间图表

你应该批量地触发回流或重绘,但是要节制地使用这些方法。尽量不处理DOM也很重要。可以使用DocumentFragment,一个轻量级的文档对象。你可以把它作为一种方法来提取文档树的一部分,或创建一个新的文档“片段”。与其不断地添加DOM节点,不如使用文档片段后只执行一次DOM插入操作,以避免过多的回流。

例如,我们写一个函数给一个元素添加20个div。如果只是简单地每次append一个div到元素中,这会触发20次回流。

function addDivs(element) {
 var div;
 for (var i = 0; i < 20; i ++) {
 div = document.createElement('div');
 div.innerHTML = 'Heya!';
 element.appendChild(div);
 }
}

要解决这个问题,可以使用DocumentFragment来代替,我们可以每次添加一个新的div到里面。完成后将DocumentFragment添加到DOM中只会触发一次回流。

function addDivs(element) {
 var div; 
 // Creates a new empty DocumentFragment.
 var fragment = document.createDocumentFragment();
 for (var i = 0; i < 20; i ++) {
 div = document.createElement('a');
 div.innerHTML = 'Heya!';
 fragment.appendChild(div);
 }
 element.appendChild(fragment);
}

可以参阅 Make the Web FasterJavaScript Memory OptimizationFinding Memory Leaks

JS内存泄漏探测器

为了帮助发现JavaScript内存泄漏,谷歌的开发人员((Marja Hölttä和Jochen Eisinger)开发了一种工具,它与Chrome开发人员工具结合使用,检索堆的快照并检测出是什么对象导致了内存泄漏。

고성능 자바스크립트 작성(번역)_자바스크립트 팁

一个JavaScript内存泄漏检测工具

有完整的文章介绍了如何使用这个工具,建议你自己到内存泄漏探测器项目页面看看。

如果你想知道为什么这样的工具还没集成到我们的开发工具,其原因有二。它最初是在Closure库中帮助我们捕捉一些特定的内存场景,它更适合作为一个外部工具。

V8优化调试和垃圾回收的标志位

Chrome支持直接通过传递一些标志给V8,以获得更详细的引擎优化输出结果。例如,这样可以追踪V8的优化:

"/Applications/Google Chrome/Google Chrome" --js-flags="--trace-opt --trace-deopt"

Windows用户可以这样运行 chrome.exe –js-flags=”–trace-opt –trace-deopt”

在开发应用程序时,下面的V8标志都可以使用。

  • trace-opt - 최적화된 함수의 이름을 기록하고 최적화 프로그램이 최적화 방법을 몰라 건너뛴 코드를 표시합니다.
  • trace-deopt - 런타임 시 "최적화 해제"될 코드를 기록합니다.
  • trace-gc - 각 가비지 수집을 기록합니다.

V8의 처리 스크립트는 *(별표)를 사용하여 최적화된 기능을 식별하고 ~(물결표)를 사용하여 최적화되지 않은 기능을 나타냅니다.

V8의 플래그와 V8의 내부 작동 방식에 대해 더 자세히 알고 싶다면 Vyacheslav Egorov의 V8 내부에 대한 우수 게시물을 읽어 보시기 바랍니다.

고해상도 시간 및 탐색 타이밍 API

고정밀 시간(HRT)은 시스템 시간 및 사용자 조정에 영향을 받지 않는 밀리초 미만의 고정밀 시간 인터페이스로, 새로운 날짜 및 날짜보다 더 발전된 인터페이스라고 할 수 있습니다. now() 정확한 측정 방법. 이는 벤치마크를 작성할 때 많은 도움이 됩니다.

고성능 자바스크립트 작성(번역)_자바스크립트 팁

고정밀 시간(HRT)은 현재 밀리초 미만의 시간 정확도를 제공합니다

현재 HRT는 Chrome(안정 버전)에서는 window.고성능 자바스크립트 작성(번역)_자바스크립트 팁.webkitNow()를 통해 사용되고 있지만, Chrome Canary에서는 접두어가 삭제되어 window.고성능 자바스크립트 작성(번역)_자바스크립트 팁.now()를 통해 호출할 수 있습니다. Paul Irish는 HTML5Rocks에 HRT에 대해 자세히 썼습니다.

이제 정확한 현재 시간을 알았으니 페이지 성능을 정확하게 측정할 수 있는 API가 있을까요? 이제 Navigation Timing API를 사용할 수 있습니다. 이 API는 웹 페이지가 로드되어 사용자에게 표시될 때 정확하고 상세한 시간 측정 기록을 얻는 간단한 방법을 제공합니다. 콘솔에서 window.고성능 자바스크립트 작성(번역)_자바스크립트 팁.timing을 사용하여 시간 정보를 얻을 수 있습니다.

고성능 자바스크립트 작성(번역)_자바스크립트 팁

콘솔에 표시되는 시간 정보

위 데이터에서 responseEnd – fetchStart의 네트워크 지연, loadEventEnd – responseEnd의 페이지 로딩 시간, loadEventEnd – NavigationStart의 탐색 및 페이지 로딩 시간 처리 등 유용한 정보를 많이 얻을 수 있습니다.

보시다시피,고성능 자바스크립트 작성(번역)_자바스크립트 팁.memory 속성은 총 힙 크기와 같은 JavaScript 메모리 데이터 사용량도 표시할 수 있습니다.

Navigation Timing API에 대한 자세한 내용은 Sam Dutton의 탐색 타이밍으로 페이지 로드 속도 측정을 읽어보세요.

정보:메모리 및 정보:추적

About:고성능 자바스크립트 작성(번역)_자바스크립트 팁 in Chrome은 브라우저의 성능 보기를 제공하여 Chrome의 모든 스레드, 탭 페이지 및 프로세스를 기록합니다.

고성능 자바스크립트 작성(번역)_자바스크립트 팁

About:Tracing提供了浏览器的性能视图

이 도구의 정말 유용한 점은 Chrome의 실행 데이터를 캡처하여 JavaScript 실행을 적절하게 조정하거나 리소스 로딩을 최적화할 수 있다는 것입니다.

Lilli Thompson은 About:고성능 자바스크립트 작성(번역)_자바스크립트 팁을 사용하여 WebGL 게임을 분석하는 방법에 대해 게임 개발자를 위해 작성된 기사를 작성했습니다.

Chrome 탐색 표시줄에 about:memory를 입력하면 매우 실용적입니다. 각 탭 페이지의 메모리 사용량을 확인할 수 있어 메모리 누수를 찾는 데 매우 유용합니다.

요약

JavaScript 세계에는 숨겨진 함정이 많습니다. 그리고 성능을 향상시킬 수 있는 만병통치약은 없습니다. 일부 최적화 솔루션을 (실제) 테스트 환경에 포괄적으로 적용해야만 최대 성능 향상을 얻을 수 있습니다. 그렇더라도 엔진이 코드를 해석하고 최적화하는 방법을 이해하면 애플리케이션을 조정하는 데 도움이 될 수 있습니다.

측정하고, 이해하고, 수정하세요. 이 과정을 계속 반복하세요.

고성능 자바스크립트 작성(번역)_자바스크립트 팁

이미지 출처: 샐리 헌터

최적화에 주의를 기울이는 것을 잊지 마세요. 하지만 편의를 위해 몇 가지 작은 최적화를 삭제할 수 있습니다. 예를 들어 일부 개발자는 for 및 for..in 루프 대신 .forEach 및 Object.keys를 선택합니다. 이는 느리지만 사용하기 더 편리합니다. 명확한 머리를 갖고 어떤 최적화가 필요하고 어떤 최적화가 필요하지 않은지 확인하십시오.

또한 JavaScript 엔진이 점점 더 빨라지고 있지만 다음 실제 병목 현상은 DOM이라는 점에 유의하세요. 리플로우와 다시 그리기를 줄이는 것도 중요하므로 필요할 때까지 DOM을 건드리지 마세요. 주의해야 할 또 다른 사항은 네트워크입니다. 특히 모바일 단말기에서는 HTTP 요청이 중요하므로 리소스 로딩을 줄이기 위해 HTTP 캐싱을 사용해야 합니다.

이러한 사항을 기억하시면 이 기사에서 대부분의 정보를 얻으실 수 있을 것입니다.

원문: http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/

저자: 애디 오스마니

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.