let과 var의 성능 비교

if, for 등의 block statement 외부에서 let으로 선언한 변수를 statement 내부에서 호출할 때에는 비용이 발생하기 때문에, 블록스코프의 영향을 받지 않는 var로 선언한 변수를 호출할 때보다 느리므로, 일반적으로 var를 쓰는 편이 낫다.

라는 논지의 글을 읽었다. 정말로 그러한지 궁금하여 ES6의 let과 기존의 var의 성능 차이를 비교실험 해보면서 그 내용을 정리하여 포스팅한다.

모든 테스트는 MacOS 10.11.6 에서 진행하였고, 테스트한 브라우저의 버전은 각 Chrome 55.0.2283.95, Firefox 50.1.0, Safari 10.0.2 이다.

테스팅 방식

가급적 단순한 소스로 빠르게 테스트를 하기 위해, 테스팅 방법은 다음 함수를 이용하였다. 모든 테스트는 세 번씩 독립시행하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const timeCheck = (times, test) => {
var res = 0,
t1,
t2
while (times-- > 0) {
t1 = window.performance.now()
test()
t2 = window.performance.now()
res += t1 - t2
}
console.log(test.name, res / times)
}
const compare = (times, ...tests) => {
tests.forEach(test => timeCheck(times, test))
}

for문 내부에서 변수 선언

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
const testVar = function() {
for(var i = 0; i <= 10000; i++) { i * 10 }
}
const testLet = () => {
for(let i = 0; i <= 10000; i++) { i * 10 }
}
compare(100000, testVar, testLet);


/* 결과 */

// Chrome
testVar 504.3849999997001
testLet 1814.029999999897

testVar 515.3349999994971
testLet 2168.7300000004616

testVar 512.150000000518
testLet 2162.0000000004075

// Firefox
testVar 377.39500000001044
testLet 392.5850000000137

testVar 369.3049999999894
testLet 362.095000000103

testVar 369.91000000006534
testLet 360.53500000012355

// Safari
testVar 660.6800000000158
testLet 7921.925000000066

testVar 636.9849999999497
testLet 8070.989999999889

testVar 659.1450000000623
testLet 8052.3949999999895
  • 크롬 : 3~4배 정도의 성능차이가 보인다. var는 블록스코프에 제한되지 않으며 재선언시 기존 변수를 그대로 활용하는 반면, let은 for문의 블록스코프에 의해 iterating 과정에서 매 번 새로 선언되므로, 이러한 차이는 당연한 결과인 듯 하다.
  • 파이어폭스 : 둘 사이에 차이가 없다. 블록스코프에 대한 성능최적화가 잘 이뤄진 것 같다.
  • 사파리 : let의 성능 저하가 심각하다;;

for문 외부에서 변수 선언

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
const testVar = function() {
var i = 0
for( ; i <= 10000; i++) { i * 10 }
}
const testLet = () => {
let i = 0
for( ; i <= 10000; i++) { i * 10 }
}
compare(100000, testVar, testLet);

/* 결과 */

// Chrome
testVar 567.7750000001979
testLet 562.8849999998911

testVar 552.2350000001934
testLet 556.7149999999783

testVar 580.739999999023
testLet 558.8600000011284

// Firefox
testVar 368.3499999999258
testLet 361.55000000003383

testVar 385.23500000002605
testLet 391.53000000022075

testVar 385.8400000003203
testLet 386.88999999985754

// Safari
testVar 671.9050000000643
testLet 680.2949999999109

testVar 649.0050000001429
testLet 677.1050000001269

testVar 644.2449999999953
testLet 678.655000000108

세 브라우저 모두 엎치락 뒤치락 한다. 유의미한 차이가 있다고 보기는 힘들다.
그러나 이론상 이 테스트는 동등한 조건의 비교가 아니다. let의 경우 for문 내부의 블록스코프로 인해 내부에서는 스코프 외부의 변수를 호출하는 것이기 때문이다.

var와 let의 혼용 비교

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
const testVar = function() {
var i = 0, sum = 0;
for( ; i <= 10000; i++) { sum += i; }
}
const testLet = function() {
let i = 0, sum = 0;
for( ; i <= 10000; i++) { sum += i; }
}
const testVarAndLet1 = function() {
let i = 0;
var sum = 0;
for( ; i <= 10000; i++) { sum += i; }
}
const testVarAndLet2 = function() {
var i = 0;
let sum = 0;
for( ; i <= 10000; i++) { sum += i; }
}
compare(100000, testVar, testLet, testVarAndLet1, testVarAndLet2);

/* 결과 */

// Chrome
testVar 328.94750000005934
testLet 1971.6525000000265
testVarAndLet1 329.54000000009637
testVarAndLet2 1987.0774999999967

testVar 329.9424999997682
testLet 1938.6849999998158
testVarAndLet1 337.06249999996726
testVarAndLet2 1977.209999999879

testVar 332.9599999998718
testLet 1969.6900000002897
testVarAndLet1 335.554999999782
testVarAndLet2 1982.3450000001758

// Firefox
testVar 564.0900000000029
testLet 562.0799999999808
testVarAndLet1 584.0799999999813
testVarAndLet2 568.134999999998

testVar 554.6249999998327
testLet 575.6050000002069
testVarAndLet1 564.4250000000866
testVarAndLet2 576.765000000104

testVar 551.2000000002226
testLet 591.664999999859
testVarAndLet1 592.3350000000064
testVarAndLet2 575.719999999892

// Safari
testVar 1216.0200000000077
testLet 1213.5549999999967
testVarAndLet1 2708.7750000000124
testVarAndLet2 2705.750000000038

testVar 1196.7449999999117
testLet 1210.9099999998725
testVarAndLet1 2687.62000000017
testVarAndLet2 2707.380000000063

testVar 1201.1799999999712
testLet 1236.5599999998667
testVarAndLet1 2702.5649999999005
testVarAndLet2 2728.939999999857
  • 크롬 : 흥미로운 결과이지만, 스코프에 따른 비용을 생각하면 당연한 결과일 수도 있겠다. for문 외부에서 let으로 선언한 sum에 for문 내부에서 접근하기 위해서는 블록스코프 체이닝을 한 단계 거쳐야 하기 때문에 비용이 발생한다는 것이다. 아마도 원글에서는 이 부분을 말하고자 했던 것 같다.
  • 파이어폭스 : 네 가지 테스트에 대해 아무런 차이가 없다. 파이어폭스만 놓고 보자면 블록스코프 체이닝으로 인한 성능저하는 고려할 필요가 없을 것 같다.
  • 사파리 : 특이하게 var만 사용한 경우나 let만 사용한 경우엔 성능이 비슷한 반면, 혼용하면 느려진다. ES6전용엔진, ES5전용엔진, ES5 + ES6 엔진이 각각 마련되어 있으며, 혼용엔진의 성능이 좀 떨어지는 것이 아닐까 추측된다.

그렇다면 내장 메소드를 활용한다면 어떨까?

for문을 forEach로 대체한 경우와 reduce를 이용한 직접계산 방식을 테스트해보자.

1. forEach로 전환

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
const testVar = function() {
var sum = 0;
new Array(10000).fill(0).forEach((v, i) => { sum += i; });
}
const testLet = function() {
let sum = 0;
new Array(10000).fill(0).forEach((v, i) => { sum += i; });
}
compare(10000, testVar, testLet);

/* 결과 */

// Chrome
testVar 916.9325000000117
testLet 1261.5850000000355

testVar 918.6650000000309
testLet 1298.54500000001

testVar 884.2624999999607
testLet 1285.3525000000227

// Firefox
testVar 874.3749999999818
testLet 828.920000000031

testVar 800.085000000101
testLet 812.5499999999629

testVar 814.4299999999675
testLet 796.349999999984

// Safari
testVar 3708.769999999984
testLet 3811.210000000001

testVar 3771.770000000024
testLet 3784.889999999963

testVar 3831.679999999993
testLet 3850.014999999996

for문으로 돌린 것보다 느려지는 것은 당연하다.

  • 크롬 : var가 살짝 빠르다. forEach로 같은 연산을 수행하기 위해서는 외부 스코프의 변수 sum을 갱신하여야 하는데, 이런 경우에는 letvar보다 더 큰 비용을 필요로 하는 것으로 보인다.
  • Firefox : 동일한 성능을 보인다. 결국 브라우저의 최적화 정도에 따라 다른 결론이 나온다고 볼 수밖에 없겠다.
  • 사파리 : 테스트를 그만두고 싶다. 이걸 왜 하고 있는거지…? 앞으로는 사파리 테스트는 한 번만 진행하겠다.

2. reduce로 직접 계산

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
const testVar = function() {
var sum = new Array(10000).fill(0).reduce((a, b, i) => a + i, 0);
}
const testLet = function() {
let sum = new Array(10000).fill(0).reduce((a, b, i) => a + i, 0);
}
compare(10000, testVar, testLet);

/* 결과 */

// Chrome
testVar 904.6750000000009
testLet 915.0900000000024

testVar 903.6025000000136
testLet 892.7199999999839

testVar 891.3525000000091
testLet 882.7275000000318

// Firefox
testVar 710.5149999999967
testLet 737.0249999999924

testVar 708.6700000000383
testLet 744.3350000000064

testVar 710.0099999999929
testLet 730.4449999999615

// Safari
testVar 4057.314999999999
testLet 4090.555000000005

한편 reduce를 이용하면 메소드 내부에서 외부 스코프의 변수를 호출할 일이 없으므로 상대적으로 매우 양호한 성능을 보이며, letvar 사이의 차이는 없는 것으로 확인된다.
이러한 차이에 대해 보다 자세히 확인하기 전에, 변인을 통제하기 위해 스코프 자체의 생성 비용을 먼저 확인해볼 필요가 있을 것 같다.

내부 스코프가 없는 경우 vs. 즉시실행함수 vs. 블록스코프

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const testNoScopeVar = function() {
var sum = new Array(1000).fill(0).join('');
}
const testNoScopeLet = function() {
let sum = new Array(1000).fill(0).join('');
}

const testFunctionScope = function() {
(function(){
var sum = new Array(1000).fill(0).join('');
})();
}
const testBlockScope = function() {
{
let sum = new Array(1000).fill(0).join('');
}
}
compare(100000, testNoScopeVar, testNoScopeLet, testFunctionScope, testBlockScope);

/* 결과 */

// Chrome
testNoScopeVar 1601.9000000000224
testNoScopeLet 1586.9125000000586
testFunctionScope 1588.4724999999517
testBlockScope 1582.192499999961

testNoScopeVar 1594.0849999999155
testNoScopeLet 1593.0999999999785
testFunctionScope 1706.752500000075
testBlockScope 1641.9625000000942

testNoScopeVar 1670.175000000112
testNoScopeLet 1708.109999999855
testFunctionScope 1678.099999999955
testBlockScope 1670.0550000006842

// Firefox
testNoScopeVar 2152.220000000035
testNoScopeLet 2145.2300000000196
testFunctionScope 2198.4799999999686
testBlockScope 2163.9349999999777

testNoScopeVar 2211.5000000000255
testNoScopeLet 2295.075000000037
testFunctionScope 2262.6200000000536
testBlockScope 2184.70499999994

testNoScopeVar 2263.599999999693
testNoScopeLet 2202.645000000368
testFunctionScope 2303.4850000000224
testBlockScope 2238.1649999999863

// Safari
testNoScopeVar 1201.3650000000011
testNoScopeLet 1180.3599999999997
testFunctionScope 1215.030000000006
testBlockScope 1207.0399999999972

testNoScopeVar 1201.0800000000418
testNoScopeLet 1217.4399999999187
testFunctionScope 1215.8149999999368
testBlockScope 1200.325000000099

testNoScopeVar 1202.5349999998944
testNoScopeLet 1195.4449999999779
testFunctionScope 1192.2499999997235
testBlockScope 1196.464999999982

기존까지의 테스트와 달리 이번 테스트는 파이어폭스가 가장 느리게 나왔으며, 사파리의 약진이 돋보여 한 번만 하겠다는 다짐을 깨고 세 번 돌려보았다.
놀랍게도 세 브라우저 모두 스코프 생성 자체는 성능상에 거의 아무런 영향을 주지 않는 것으로 확인된다. 혹시 스코프를 한 번만 생성했기 때문에 영향이 없었던 것은 아닐까? 스코프를 잔뜩 생성해서 테스트 해보자.

스코프를 1000회 생성

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
const testFunctionScope = function() {
new Array(1000).fill(0).forEach(_=> {
(function(){
var sum = new Array(1000).fill(0).join('');
})();
});
}
const testBlockScope = function() {
new Array(1000).fill(0).forEach(_=> {
{
let sum = new Array(1000).fill(0).join('');
}
});
}
compare(1000, testFunctionScope, testBlockScope);

/* 결과 */

// Chrome
testFunctionScope 16264.852499999985
testBlockScope 15813.532500000016

testFunctionScope 16406.502499999522
testBlockScope 16261.40749999987

testFunctionScope 16506.43499999994
testBlockScope 16097.730000000083

// Firefox
testFunctionScope 21992.190000000028
testBlockScope 21433.97000000001

testFunctionScope 21121.65000000008
testBlockScope 21132.1000000005

testFunctionScope 22201.30000000063
testBlockScope 21140.65000000017

// Safari
testFunctionScope 11941.540000000026
testBlockScope 11820.930000000008

testFunctionScope 12206.484999999979
testBlockScope 12454.840000000018

testFunctionScope 12273.975000000224
testBlockScope 12344.614999999932

이정도면 블록스코프와 즉시실행함수의 속도차이는 없다고 보아야 할 것이다. 그렇다면 이제 letvar가 외부 스코프의 변수를 갱신하는 데에 드는 비용을 확인해볼 수 있겠다. Safari에 대해 안좋게 평가했던 내 자신을 반성한다.

외부스코프에 대한 비용 비교 - 1

반복을 위한 함수선언 자체가 새로운 스코프를 만들게 되므로, 이런 변인을 통제(새로운 스코프 형성 없이 테스트)하기 위해 무식한 소스를 작성해보았다.

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
const testFunctionScopeVar = function() {
var sum = new Array(10000).fill(0);
(function(){ sum = sum.join(''); })();
sum = new Array(10000).fill(0);
(function(){ sum = sum.join(''); })();
sum = new Array(10000).fill(0);
(function(){ sum = sum.join(''); })();
sum = new Array(10000).fill(0);
(function(){ sum = sum.join(''); })();
sum = new Array(10000).fill(0);
(function(){ sum = sum.join(''); })();
}
const testFunctionScopeLet = function() {
let sum = new Array(10000).fill(0);
(function(){ sum = sum.join(''); })();
sum = new Array(10000).fill(0);
(function(){ sum = sum.join(''); })();
sum = new Array(10000).fill(0);
(function(){ sum = sum.join(''); })();
sum = new Array(10000).fill(0);
(function(){ sum = sum.join(''); })();
sum = new Array(10000).fill(0);
(function(){ sum = sum.join(''); })()
}

const testBlockScopeVar = function() {
var sum = new Array(10000).fill(0);
{ sum = sum.join(''); }
sum = new Array(10000).fill(0);
{ sum = sum.join(''); }
sum = new Array(10000).fill(0);
{ sum = sum.join(''); }
sum = new Array(10000).fill(0);
{ sum = sum.join(''); }
sum = new Array(10000).fill(0);
{ sum = sum.join(''); }
}
const testBlockScopeLet = function() {
let sum = new Array(10000).fill(0);
{ sum = sum.join(''); }
sum = new Array(10000).fill(0);
{ sum = sum.join(''); }
sum = new Array(10000).fill(0);
{ sum = sum.join(''); }
sum = new Array(10000).fill(0);
{ sum = sum.join(''); }
sum = new Array(10000).fill(0);
{ sum = sum.join(''); }
}

compare(1000, testFunctionScopeVar, testFunctionScopeLet, testBlockScopeVar, testBlockScopeLet);

/* 결과 */

// Chrome
testFunctionScopeVar 798.4200000000856
testFunctionScopeLet 781.6874999999345
testBlockScopeVar 786.3724999999904
testBlockScopeLet 779.984999999986

testFunctionScopeVar 798.8450000002049
testFunctionScopeLet 850.8949999999168
testBlockScopeVar 860.9525000000867
testBlockScopeLet 822.717499999897

testFunctionScopeVar 829.2199999999721
testFunctionScopeLet 821.3075000001991
testBlockScopeVar 856.947499999922
testBlockScopeLet 799.0949999999866

// Firefox
testFunctionScopeVar 939.735000000006
testFunctionScopeLet 13033.770000000011
testBlockScopeVar 13230.579999999976
testBlockScopeLet 13146.650000000001

testFunctionScopeVar 13114.275000000023
testFunctionScopeLet 13361.729999999778
testBlockScopeVar 3324.755000000092
testBlockScopeLet 13032.479999999865

testFunctionScopeVar 13281.810000000201
testFunctionScopeLet 13104.510000000242
testBlockScopeVar 13282.044999999867
testBlockScopeLet 13141.575000000244

// Safari
testFunctionScopeVar 631.1549999999843
testFunctionScopeLet 623.5099999999984
testBlockScopeVar 641.4000000000196
testBlockScopeLet 622

testFunctionScopeVar 661.0600000000013
testFunctionScopeLet 652.924999999992
testBlockScopeVar 630.9400000000023
testBlockScopeLet 644.5299999999916

testFunctionScopeVar 658.5400000000045
testFunctionScopeLet 652.0749999999971
testBlockScopeVar 645.9750000000058
testBlockScopeLet 644.9399999999987

이상하다. 앞서 테스트에서는 분명 블록스코프를 형성하는 for문에서 외부스코프에 접근할 때에 비용 차이가 있었는데, 이번에는 그 차이가 전혀 보이지 않는다. for문을 가지고 다른 변인통제장치를 마련하여 다시 한 번 테스트 해보자. ( 파이어폭스가 잠깐 넋이 나간걸까… ? )

외부스코프에 대한 비용 비교 - 2. for문

for문은 자체적으로 블록스코프를 형성하므로, 블록스코프와 즉시실행함수의 성능 차이가 거의 없다는 전제하에 var에 대해서도 let과 마찬가지로 외부스코프의 변수에 접근하게끔 for문 내부에 즉시실행함수를 넣어보았다.

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
const testVarNoScope = function() {
var sum = 0;
for(var i = 0 ; i <= 10000; i++) {
sum += i;
}
}
const testVar = function() {
var sum = 0;
for(var i = 0 ; i <= 10000; i++) {
(function(){ sum += i; })();
}
}
const testLet = function() {
let sum = 0;
for(let i = 0 ; i <= 10000; i++) {
sum += i;
}
}
const testLetWithFunctionScope = function() {
let sum = 0;
for(let i = 0 ; i <= 10000; i++) {
(function(){ sum += i; })();
}
}
const testLetWithBlockScope = function() {
let sum = 0;
for(let i = 0 ; i <= 10000; i++) {
{ sum += i; }
}
}
compare(10000, testVarNoScope, testVar, testLet, testLetWithFunctionScope, testLetWithBlockScope);


/* 결과 */

// Chrome
testVarNoScope 35.77249999992273
testVar 1163.234999999855
testLet 504.2325000000492
testLetWithFunctionScope 4295.67250000003
testLetWithBlockScope 500.6700000000783

testVarNoScope 36.612500000046566
testVar 1221.842499999766
testLet 518.9699999999648
testLetWithFunctionScope 4509.729999999974
testLetWithBlockScope 549.4775000002774

testVarNoScope 36.07249999981286
testVar 1247.2349999999278
testLet 518.7849999999453
testLetWithFunctionScope 4420.822500000126
testLetWithBlockScope 504.9025000000038

// Firefox
testVarNoScope 55.274999999963256
testVar 76.43499999997948
testLet 55.2449999999626
testLetWithFunctionScope 42008.84499999993
testLetWithBlockScope 52.959999999948195

testVarNoScope 55.64499999990221
testVar 222.23999999988882
testLet 51.19499999989057
testLetWithFunctionScope 40998.735000000204
testLetWithBlockScope 52.89000000010128

testVarNoScope 54.12999999962631
testVar 225.33499999943888
testLet 51.91500000018277
testLetWithFunctionScope 42197.229999999894
testLetWithBlockScope 52.25500000020838

// Safari
testVarNoScope 119.68999999999824
testVar 2372.4249999999956
testLet 837.6250000000036
testLetWithFunctionScope 3112.0200000000023
testLetWithBlockScope 847.4800000000105

testVarNoScope 112.06999999999607
testVar 2435.275000000034
testLet 832.6699999999837
testLetWithFunctionScope 3124.41
testLetWithBlockScope 843.8000000000211

testVarNoScope 116.38000000001557
testVar 2436.8349999999846
testLet 876.3200000000943
testLetWithFunctionScope 3205.7199999999684
testLetWithBlockScope 875.5450000000201
  • 크롬 : 스코프가 전혀 중첩되지 않은 첫번째의 결과가 가장 뛰어나고, 그 다음으로는 블록스코프 하나(for문 자체)로 이루어진 세번째 및 블록스코프 둘(for문 자체 + 내부)로 이루어진 다섯번째 결과가 동일한 성능을 보이고 있다. 반면 즉시실행함수는 상당한 비용을 소모하는 것으로 확인된다. 네번째 결과가 훨씬 높게 나타난 이유는 두번째와 비교해 스코프 중첩이 한 번 더 있기 때문이 아닐까 추측된다.
  • 파이어폭스 : 스코프가 없는 상태의 var(testVarNoScope)보다도 블록스코프가 있는 상태(for)의 let의 결과(testLet)가 더욱 좋은 성능을 발휘한다. 모든 면에서 ES5 이하의 기능으로 구현한 소스보다 ES6에서 추가된 기능들이 더욱 좋은 성능을 보인다.
  • 사파리 : 크롬보다는 좀더 빠른 성능을 보이고 있는데, 결과 사이의 상대적 차이는 크롬과 비슷하다.

이 테스트는 영 개운치 않다. 앞선 테스트에서는 블록스코프와 즉시실행함수 사이에 성능차이가 없었는데, for문 내부에서 호출한 즉시실행함수 만큼은 모든 브라우저에서 현저히 느리다.

let이나 const 선언이 없다면 블록스코프가 생성되지 않는가?

tc39의 ECMAScript2015 Specfication - Lexical Environments는 다음과 같이 기술하고 있다.

A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. (중략) Usually a Lexical Environment is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement and a new Lexical Environment is created each time such code is evaluated.

(발번역 주의) 렉시컬 환경은 ECMAScript 코드의 렉시컬 중첩구조를 기반으로 식별자(identifier)와 특정 변수 및 함수와의 연관성을 정의하기 위한 스펙 유형이다. 렉시컬 환경은 일반적으로 Function Declaration, Block Statement, try구문의 catch 절과 같은 ECMAScript 코드의 특정 구문 구조와 연결되며, 이러한 코드가 평가될 때마다 새로운 렉시컬 환경이 생성된다.

즉 ‘block statement’가 평가될 때마다 새로운 렉시컬 환경이 생성되며, 이 때 블록스코프가 생성되는 것으로 보아야 할 것이다. let이나 const 선언 유무와 무관하게 블록스코프는 무조건 형성된다. 기존의 함수 내부에 var 변수를 선언하든 하지 않든 함수스코프가 생성되는 것에는 어떠한 영향도 주지 않는 것과 마찬가지로 말이다.

요약 및 결론

  • 각 브라우저별로 상황에 따라 상대적으로 빠른 연산을 수행하기도 하고 반대로 매우 느리게 처리하기도 하는 등, 성능이 다 다르다. 테스트 결과 특별히 어느 브라우저가 제일 뛰어나다는 판단은 내릴 수 없겠다.

  • 블록스코프와 즉시실행함수 자체의 비용 차이는 크지 않은 것으로 확인되었다.

  • 다만 for문 내부에 즉시실행함수를 삽입할 경우에는 모든 브라우저에서 매우 느리게 동작한다.

  • 스코프체이닝으로 외부 변수에 엑세스할 때의 비용 역시 varlet이 큰 차이를 보이지 않는다. 다만 파이어폭스의 경우 let이 var보다도 조금 더 빠른 성능을 보이고, 사파리의 경우 둘을 함께 쓸 경우 배로 느려진다.

  • iffor처럼 그 자체가 블록스코프를 지니는 경우, 그 중에서도 block statement 외부의 변수를 내부에서 사용하는 경우에는, 크롬 및 사파리의 경우 var를 활용하는 편이 더 좋은 성능을 발휘하는 반면, 파이어폭스는 let을 그대로 사용하는 편이 더 낫다.

  • 스코프 체이닝을 최소화하는 것이 좋다는 것은 당연한 상식이다. 그러나 이는 어디까지나 이론상 그렇다는 것이고, 테스트 결과 엔진 내부 로직에 따라(어떻게 구현되었는지는 모르겠지만 아무튼) 블록스코프가 성능에 거의 영향을 주지 않는 경우도 있는 것으로 확인된다(파이어폭스).

  • 일반적인 경우 var 변수가 더 효율적 인지 여부는 브라우저마다, 상황마다 다르다. 기존의 코딩스타일 안에서 새로운 문법시스템을 판단하는 것 자체가 잘못된 접근일 수 있다는 생각도 든다. 기왕 블록스코프가 도입된 이상 블록스코프 내에서 독립적으로 처리할 방안(reduce 등)을 고민하고, 마땅한 수단이 없는 경우에 한해 부득이 외부 변수를 호출하되, var를 쓸지 let을 쓸지는 타겟 브라우저에 따라 판단해야 할 것 같다.

  • 불과 1년 전 var와 let의 성능비교 테스트에 대한 블로그 포스팅을 읽은 적이 있는데, 당시에는 varlet보다 압도적으로 빠르게 연산을 수행했던 것으로 기억한다. 그 1년 사이 둘의 성능은 같아졌다. 그만큼 최적화가 이루어져왔었기 때문이다. 그렇다면 앞서 확인했던 외부스코프에 대한 접근 성능 역시 점차 최적화가 될 것이라 기대한다.

  • 세 브라우저가 각각의 상황에서 저마다 다른 성능을 보여주었다. 즉 최적화가 얼마나 어떻게 진행되었는지에 따라 성능은 얼마든지 달라질 수 있는 문제이며, 현재의 결론이 1년 뒤에는 또 어떻게 달라질지도 모를 일이다.

  • 개인적으로는 파이어폭스처럼 다른 브라우저들도 외부스코프에 대한 접근 성능이 충분히 최적화될 것이라 기대하면서 지금부터 그냥 let을 쓰겠다. 혼용하는 편이 더 헷갈림.