수색…
소개
JavaScript는 다른 언어와 마찬가지로 특정 언어 기능을 사용하는 데있어 신중해야합니다. 일부 기능을 과도하게 사용하면 성능이 저하 될 수 있지만 일부 기술은 성능을 향상시키는 데 사용될 수 있습니다.
비고
조숙 한 최적화는 모든 악의 뿌리임을 기억하십시오. 명확하고 정확한 코드를 작성한 다음 성능 문제가있는 경우 프로파일 러를 사용하여 개선 할 특정 영역을 찾습니다. 의미있는 방식으로 전반적인 성능에 영향을 미치지 않는 코드 최적화 시간을 낭비하지 마십시오.
측정, 측정, 측정. 성과는 종종 직관력이 떨어지며 시간이 지남에 따라 변합니다. 더 빨라진 것이 미래에 없을 수도 있고 사용 사례에 달려있을 수도 있습니다. 최적화가 실제로 이루어지고 성능이 저하되지 않고 변경이 가치가 있는지 확인하십시오.
성능이 중요한 기능에서 try / catch 방지
일부 JavaScript 엔진 (예 : Ignition + turbofan 이전의 Node.js 및 이전 버전의 Chrome)은 try / catch 블록이 포함 된 함수에서 최적화 프로그램을 실행하지 않습니다.
성능이 중요한 코드에서 예외를 처리해야하는 경우에는 try / catch를 별도의 기능으로 유지하는 것이 더 빠를 수 있습니다. 예를 들어,이 함수는 일부 구현에 의해 최적화되지 않습니다.
function myPerformanceCriticalFunction() {
try {
// do complex calculations here
} catch (e) {
console.log(e);
}
}
그러나 리팩터링을 통해 느린 코드를 별도의 함수 (최적화 할 수 있음)로 옮기고 try
블록 내부에서 호출 할 수 있습니다.
// This function can be optimized
function doCalculations() {
// do complex calculations here
}
// Still not always optimized, but it's not doing much so the performance doesn't matter
function myPerformanceCriticalFunction() {
try {
doCalculations();
} catch (e) {
console.log(e);
}
}
다음은 차이점을 보여주는 jsPerf 벤치 마크입니다 : https://jsperf.com/try-catch-deoptimization . 대부분의 브라우저의 현재 버전에서는 차이가 크지 않지만 Chrome 및 Firefox 나 IE의 최신 버전에서는 try / catch 내부에서 도우미 함수를 호출하는 버전이 더 빠를 수 있습니다.
이와 같은 최적화는주의 깊게 작성해야하며 코드 프로파일 링을 기반으로하는 실제적인 증거가 있어야합니다. 자바 스크립트 엔진이 나아지면 도움이되지 않고 성능에 해를 끼칠 수 있습니다. 아무런 차이가 없어도 아무런 이유없이 코드가 복잡해집니다. 도움이되는지, 상처를 주는지, 아무런 차이가 없는지 여부는 많은 요인에 따라 달라질 수 있으므로 항상 코드에 미치는 영향을 측정하십시오. 이것은 모든 최적화에서 마찬가지지만, 특히 컴파일러 / 런타임의 하위 레벨 세부 사항에 의존하는 이와 같은 미세 최적화가 해당됩니다.
무거운 컴퓨팅 기능을위한 메모 작성 도구 사용
프로세서 (클라이언트 측 또는 서버 측)에서 무거울 수있는 함수를 작성하는 경우 이전 함수 실행 및 반환 값 의 캐시 인 메모 작성자 를 고려할 수 있습니다 . 이를 통해 함수의 매개 변수가 이전에 전달되었는지 확인할 수 있습니다. 순수 함수는 입력을 받고, 고유 한 출력을 반환하며, 범위 밖의 부작용을 일으키지 않으므로 예측할 수없는 함수에 메모를 추가하거나 외부 리소스 (예 : AJAX 호출 또는 무작위로)에 의존해서는 안됩니다. 반환 값).
재귀 적 계승 함수가 있다고 가정 해 봅시다.
function fact(num) {
return (num === 0)? 1 : num * fact(num - 1);
}
예를 들어 1에서 100까지의 작은 값을 전달하면 아무런 문제가 없지만 더 깊이 들어가기 시작하면 호출 스택을 날려 버리거나이 작업을 수행하는 Javascript 엔진에 약간의 어려움을 겪을 수 있습니다. 특히 엔진이 꼬리 - 호출 최적화 (tail-call optimization)로 계산되지 않는다면 (Douglas Crockford는 네이티브 ES6에는 테일 콜 최적화 (tail-call optimization)가 포함되어 있음).
우리는 자신의 사전을 1에서부터 신에 이르기까지 하드 코딩 할 수 있습니다. 즉, 해당 계승에 따라 어떤 숫자가 쓰이지 만, 제가 조언한다면 확실하지 않습니다! 메모 작성자를 만들어 보죠.
var fact = (function() {
var cache = {}; // Initialise a memory cache object
// Use and return this function to check if val is cached
function checkCache(val) {
if (val in cache) {
console.log('It was in the cache :D');
return cache[val]; // return cached
} else {
cache[val] = factorial(val); // we cache it
return cache[val]; // and then return it
}
/* Other alternatives for checking are:
|| cache.hasOwnProperty(val) or !!cache[val]
|| but wouldn't work if the results of those
|| executions were falsy values.
*/
}
// We create and name the actual function to be used
function factorial(num) {
return (num === 0)? 1 : num * factorial(num - 1);
} // End of factorial function
/* We return the function that checks, not the one
|| that computes because it happens to be recursive,
|| if it weren't you could avoid creating an extra
|| function in this self-invoking closure function.
*/
return checkCache;
}());
이제 우리는 그것을 사용할 수 있습니다 :
이제는 내가 한 일에 대해 생각하기 시작했습니다. num 에서 감소하지 않고 1에서 증가 시키면 캐시에 1에서 num 까지 모든 계승을 재귀 적으로 캐시 할 수 있었지만 그 값은 그대로 남겨 두었습니다.
이것은 훌륭하지만 여러 매개 변수 가 있다면 어떨까요? 이게 문제 야? 우리는 JSON.stringify ()를 arguments 배열에 사용하거나 함수가 의존하는 값 목록 (객체 지향 접근법)을 사용하는 것과 같은 멋진 트릭을 수행 할 수 있습니다. 이것은 모든 인수와 종속성이 포함 된 고유 키를 생성하기 위해 수행됩니다.
이전과 같은 범위 개념 (원본을 사용하고 캐시 객체에 액세스 할 수있는 새 함수를 반환)을 사용하여 다른 함수를 "메모"하는 함수를 만들 수도 있습니다.
경고 : ES6 구문은 마음에 들지 않으면 ...을 아무 것도 사용하지 않고 var args = Array.prototype.slice.call(null, arguments);
장난; const를 대체하고 var로 보자. 그리고 당신이 이미 알고있는 다른 것들.
function memoize(func) {
let cache = {};
// You can opt for not naming the function
function memoized(...args) {
const argsKey = JSON.stringify(args);
// The same alternatives apply for this example
if (argsKey in cache) {
console.log(argsKey + ' was/were in cache :D');
return cache[argsKey];
} else {
cache[argsKey] = func.apply(null, args); // Cache it
return cache[argsKey]; // And then return it
}
}
return memoized; // Return the memoized function
}
이제 이것이 여러 인수에 대해 작동하지만 객체 지향적 인 방법으로 많이 사용되지는 않을 것이라는 점에 주목하십시오. 종속성을 위해 추가 객체가 필요할 수도 있습니다. 또한, 배열의 소멸은 배열 형태가 아닌 개별적으로 보낼 func.apply(null, args)
는 func(...args)
로 대체 될 수 있습니다. 또한 참조 용으로 func에 인수로 배열을 전달하면 Function.prototype.apply
를 사용하지 않으면 작동하지 않습니다.
위의 방법을 사용하려면 다음을 수행하십시오.
const newFunction = memoize(oldFunction);
// Assuming new oldFunction just sums/concatenates:
newFunction('meaning of life', 42);
// -> "meaning of life42"
newFunction('meaning of life', 42); // again
// => ["meaning of life",42] was/were in cache :D
// -> "meaning of life42"
코드 벤치마킹 - 실행 시간 측정
대부분의 성능 정보는 JS 엔진의 현재 상태에 크게 의존하며 주어진 시간에만 관련이있을 것으로 예상됩니다. 성능 최적화의 기본 원칙은 최적화를 시도하기 전에 먼저 측정해야하며, 예상 된 최적화 후에 다시 측정해야한다는 것입니다.
코드 실행 시간을 측정하려면 다음과 같은 다양한 시간 측정 도구를 사용할 수 있습니다.
주어진 페이지에 대한 타이밍 관련 성능 정보를 나타내는 성능 인터페이스 (브라우저에서만 사용 가능).
Node.js의 process.hrtime 은 타이밍 정보를 [초, 나노초] 튜플로 제공합니다. 인수없이 호출하면 임의의 시간을 반환하지만 이전에 반환 된 값을 인수로 호출하여 두 실행 간의 차이를 반환합니다.
콘솔 타이머 console.time("labelName")
은 작업이 소요되는 시간을 추적하는 데 사용할 수있는 타이머를 시작합니다. 각 타이머에 고유 한 레이블 이름을 지정하고 주어진 페이지에서 최대 10,000 개의 타이머를 실행할 수 있습니다. console.timeEnd("labelName")
를 같은 이름으로 호출하면 브라우저는 주어진 이름의 타이머를 마친 후 타이머가 시작된 이후 경과 된 시간을 밀리 초 단위로 출력합니다. time () 및 timeEnd ()에 전달 된 문자열이 일치해야합니다. 그렇지 않으면 타이머가 완료되지 않습니다.
Date.now 함수 Date.now()
는 1970 년 1 월 1 일 00:00:00 UTC 이후 시간의 숫자 표현 인 밀리 초 단위의 현재 Timestamp 를 반환합니다. 메서드 now ()는 Date의 정적 메서드이므로 항상 Date.now ()로 사용합니다.
예제 1 : performance.now()
이 예제에서는 우리 함수의 실행을위한 경과 시간을 계산할 것이고 , 밀리 초 단위로 측정 된 DOMHighResTimeStamp 를 1000 분의 1 밀리 초로 반환하는 Performance.now () 메서드를 사용할 것입니다.
let startTime, endTime;
function myFunction() {
//Slow code you want to measure
}
//Get the start time
startTime = performance.now();
//Call the time-consuming function
myFunction();
//Get the end time
endTime = performance.now();
//The difference is how many milliseconds it took to call myFunction()
console.debug('Elapsed time:', (endTime - startTime));
콘솔의 결과는 다음과 같습니다.
Elapsed time: 0.10000000009313226
performance.now()
사용은 1/1000 밀리 초의 정확도로 브라우저에서 가장 높은 정밀도를 갖지만 가장 낮은 호환성을 제공 합니다.
예제 2 : Date.now()
이 예제에서는 큰 배열 (백만 값)의 초기화에 대한 경과 시간을 계산할 것이고 우리는 Date.now()
메서드를 사용할 것입니다
let t0 = Date.now(); //stores current Timestamp in milliseconds since 1 January 1970 00:00:00 UTC
let arr = []; //store empty array
for (let i = 0; i < 1000000; i++) { //1 million iterations
arr.push(i); //push current i value
}
console.log(Date.now() - t0); //print elapsed time between stored t0 and now
예제 3 : console.time("label")
& console.timeEnd("label")
이 예제에서는 예제 2와 동일한 작업을 수행하고 있지만 console.time("label")
및 console.timeEnd("label")
메소드를 사용하려고합니다
console.time("t"); //start new timer for label name: "t"
let arr = []; //store empty array
for(let i = 0; i < 1000000; i++) { //1 million iterations
arr.push(i); //push current i value
}
console.timeEnd("t"); //stop the timer for label name: "t" and print elapsed time
예제 4 는 process.hrtime()
Node.js 프로그램에서 이것은 소비 된 시간을 측정하는 가장 정확한 방법입니다.
let start = process.hrtime();
// long execution here, maybe asynchronous
let diff = process.hrtime(start);
// returns for example [ 1, 2325 ]
console.log(`Operation took ${diff[0] * 1e9 + diff[1]} nanoseconds`);
// logs: Operation took 1000002325 nanoseconds
전역 변수, 속성 및 색인 된 값에 대한 지역 변수 선호
자바 스크립트 엔진은 먼저 검색 범위를 더 큰 범위로 확장하기 전에 로컬 범위 내에서 변수를 찾습니다. 변수가 배열의 인덱스 값 또는 연관 배열의 속성 인 경우 내용을 찾기 전에 먼저 부모 배열을 찾습니다.
이는 성능에 중대한 코드로 작업 할 때 영향을 미칩니다. 예를 들어 일반적인 for
루프를 예로 들어 보겠습니다.
var global_variable = 0;
function foo(){
global_variable = 0;
for (var i=0; i<items.length; i++) {
global_variable += items[i];
}
}
for
루프의 모든 반복에 for
엔진은 items
을 조회하고 항목 내의 length
속성을 조회하며 items
다시 검색하고 items
색인 i
에서 값을 조회 한 다음 global_variable
조회하여 전역 범위를 확인하기 전에 먼저 로컬 범위를 먼저 시도합니다.
위 함수의 performant 재 작성은 다음과 같습니다.
function foo(){
var local_variable = 0;
for (var i=0, li=items.length; i<li; i++) {
local_variable += items[i];
}
return local_variable;
}
다시 작성된 for
루프의 모든 반복에 대해 엔진은 li
을 검색하고, items
을 검색하고, 인덱스 i
에서 값을 조회하고, local_variable
조회합니다. 이번에는 로컬 범위 만 확인하면됩니다.
다시 생성하지 않고 개체를 재사용하십시오.
예제 A
var i,a,b,len;
a = {x:0,y:0}
function test(){ // return object created each call
return {x:0,y:0};
}
function test1(a){ // return object supplied
a.x=0;
a.y=0;
return a;
}
for(i = 0; i < 100; i ++){ // Loop A
b = test();
}
for(i = 0; i < 100; i ++){ // Loop B
b = test1(a);
}
루프 B는 루프 A보다 4 배 (400 %) 빠릅니다.
성능 코드에서 새 개체를 만드는 것은 매우 비효율적입니다. 루프 A는 호출 할 때마다 새 객체를 반환하는 함수 test()
를 호출합니다. 생성 된 객체는 반복 될 때마다 폐기되고 루프 B는 객체 반환이 요구되는 test1()
을 호출합니다. 따라서 동일한 객체를 사용하고 새로운 객체의 할당 및 과도한 GC 히트를 방지합니다. (GC는 성능 테스트에 포함되지 않았다)
보기 B
var i,a,b,len;
a = {x:0,y:0}
function test2(a){
return {x : a.x * 10,y : a.x * 10};
}
function test3(a){
a.x= a.x * 10;
a.y= a.y * 10;
return a;
}
for(i = 0; i < 100; i++){ // Loop A
b = test2({x : 10, y : 10});
}
for(i = 0; i < 100; i++){ // Loop B
a.x = 10;
a.y = 10;
b = test3(a);
}
루프 B는 루프 A보다 5 배 (500 %) 빠릅니다.
DOM 업데이트 제한
브라우저 환경에서 실행될 때 JavaScript에서 흔히 볼 수있는 실수는 필요한 것보다 더 자주 DOM을 업데이트하는 것입니다.
여기서 문제는 DOM 인터페이스의 모든 업데이트로 인해 브라우저가 화면을 다시 렌더링한다는 것입니다. 업데이트가 페이지의 요소 레이아웃을 변경하면 전체 페이지 레이아웃을 다시 계산해야하며 가장 간단한 경우에도 성능이 매우 높습니다. 페이지를 다시 그리는 과정을 리플 로우 (reflow )라고하며 브라우저가 느리게 실행되거나 응답하지 않을 수도 있습니다.
문서를 너무 자주 업데이트하는 결과는 목록에 항목을 추가하는 다음 예제와 함께 설명됩니다.
<ul>
요소를 포함하는 다음 문서를 고려하십시오.
<!DOCTYPE html>
<html>
<body>
<ul id="list"></ul>
</body>
</html>
5000
항목을 5000 번 반복하여 목록에 추가합니다 (효과를 높이려면 강력한 컴퓨터에서 더 많은 수로 이것을 시도 할 수 있습니다).
var list = document.getElementById("list");
for(var i = 1; i <= 5000; i++) {
list.innerHTML += `<li>item ${i}</li>`; // update 5000 times
}
이 경우 단일 DOM 업데이트에서 5000 개의 변경 사항을 일괄 처리하여 성능을 향상시킬 수 있습니다.
var list = document.getElementById("list");
var html = "";
for(var i = 1; i <= 5000; i++) {
html += `<li>item ${i}</li>`;
}
list.innerHTML = html; // update once
document.createDocumentFragment()
함수는 루프에 의해 생성 된 HTML의 경량 컨테이너로 사용될 수 있습니다. 이 메서드는 container 요소의 innerHTML
속성을 수정하는 것보다 약간 빠릅니다 (아래 그림 참조).
var list = document.getElementById("list");
var fragment = document.createDocumentFragment();
for(var i = 1; i <= 5000; i++) {
li = document.createElement("li");
li.innerHTML = "item " + i;
fragment.appendChild(li);
i++;
}
list.appendChild(fragment);
null로 객체 프로퍼티를 초기화
모든 최신 JavaScript JIT 컴파일러는 예상되는 객체 구조를 기반으로 코드를 최적화하려고합니다. MD의 일부 팁.
다행히도 객체와 속성은 종종 "예측 가능"하며 이러한 경우에는 기본 구조도 예측 가능합니다. JIT는 예측 가능한 액세스를보다 빠르게 수행 할 수 있습니다.
객체를 예측 가능하게 만드는 가장 좋은 방법은 생성자에서 전체 구조를 정의하는 것입니다. 따라서 객체 생성 후 추가 속성을 추가하려면 생성자에서 null
사용하여 객체를 정의하십시오. 이는 옵티 마이저가 전체 라이프 사이클 동안 오브젝트 동작을 예측하는 데 도움이됩니다. 그러나 모든 컴파일러에는 다른 옵티 마이저가 있으며 성능이 향상 될 수는 있지만 전체적으로 값이 알려지지 않은 경우에도 생성자의 모든 속성을 정의하는 것이 좋습니다.
몇 가지 테스트를위한 시간. 내 테스트에서는 for 루프를 사용하여 클래스 인스턴스의 큰 배열을 만들고있다. 루프 내에서 배열 초기화 전에 모든 객체의 "x"속성에 동일한 문자열을 할당합니다. 생성자가 "x"속성을 null로 초기화하면 배열은 추가 명령문을 수행하는 경우에도 항상 더 잘 처리됩니다.
이것은 코드입니다.
function f1() {
var P = function () {
this.value = 1
};
var big_array = new Array(10000000).fill(1).map((x, index)=> {
p = new P();
if (index > 5000000) {
p.x = "some_string";
}
return p;
});
big_array.reduce((sum, p)=> sum + p.value, 0);
}
function f2() {
var P = function () {
this.value = 1;
this.x = null;
};
var big_array = new Array(10000000).fill(1).map((x, index)=> {
p = new P();
if (index > 5000000) {
p.x = "some_string";
}
return p;
});
big_array.reduce((sum, p)=> sum + p.value, 0);
}
(function perform(){
var start = performance.now();
f1();
var duration = performance.now() - start;
console.log('duration of f1 ' + duration);
start = performance.now();
f2();
duration = performance.now() - start;
console.log('duration of f2 ' + duration);
})()
Chrome과 Firefox의 결과입니다.
FireFox Chrome
--------------------------
f1 6,400 11,400
f2 1,700 9,600
우리가 볼 수 있듯이 성능 향상은 두 가지면에서 매우 다릅니다.
Numbers를 일관되게 사용하십시오.
엔진이 특정 작은 유형의 값을 사용하고 있다고 정확하게 예측할 수 있다면 실행 된 코드를 최적화 할 수 있습니다.
이 예제에서는 배열 요소를 합산하고 소요 시간을 출력하는 간단한 함수를 사용합니다.
// summing properties
var sum = (function(arr){
var start = process.hrtime();
var sum = 0;
for (var i=0; i<arr.length; i++) {
sum += arr[i];
}
var diffSum = process.hrtime(start);
console.log(`Summing took ${diffSum[0] * 1e9 + diffSum[1]} nanoseconds`);
return sum;
})(arr);
배열을 만들고 요소를합시다.
var N = 12345,
arr = [];
for (var i=0; i<N; i++) arr[i] = Math.random();
결과:
Summing took 384416 nanoseconds
이제 정수 만 사용하여 같은 작업을 해봅시다.
var N = 12345,
arr = [];
for (var i=0; i<N; i++) arr[i] = Math.round(1000*Math.random());
결과:
Summing took 180520 nanoseconds
합계 정수는 여기에서 절반의 시간이 걸렸습니다.
엔진은 자바 스크립트에서 사용하는 것과 동일한 유형을 사용하지 않습니다. 아시다시피 JavaScript의 모든 숫자는 IEEE754 배정 밀도 부동 소수점 숫자입니다. 정수에 대한 구체적인 표현은 없습니다. 그러나 엔진은 정수 만 사용한다는 것을 예측할 수 있으면 짧은 정수와 같이 더 작고 빠르며 표현을 사용할 수 있습니다.
이러한 종류의 최적화는 특히 계산 또는 데이터 집약적 인 어플리케이션에 중요합니다.