[JavaScript] 성능 최적화 기법 (1)

Wishtree·2021년 5월 2일
6

참고 :
1. 자바스크립트 성능 최적화 기법
https://www.youtube.com/watch?v=9MZl8Uq9Gmw
2. requestAnimationFrame
https://www.youtube.com/watch?v=9XnqDSabFjM
3. Optimizing p5.js Code for Performance
https://github.com/processing/p5.js/wiki/Optimizing-p5.js-Code-for-Performance
4. 13 Tips to Write Faster, Better-Optimized JavaScript
https://medium.com/@bretcameron/13-tips-to-write-faster-better-optimized-javascript-dc1f9ab063d8
5. How to improve JavaScript Performance
https://javascript.plainenglish.io/how-to-improve-javascript-performance-f75f09835eb6

배경

  • 웹사이트를 구성하는 자바스크립트의 양적 증가 추세

  • 브라우저 성능의 발달로 인한 자유도 증가가 역설적으로 웹페이지 성능 하락을 야기

    • 성능 문제의 대표적인 증상
      1. '반환하지 않은 스크립트'
      2. 브라우저의 스크립트 구문 제한
      3. 랜딩페이지 응답 지연(로딩)과 구동 지연(먹통)
      4. 스타일시트 적용 지연

성능 문제를 다루기 전에

  • 좋은 프로파일러로 문제를 진단하여 원인을 좁혀야 한다.
  1. F12 Developer tool (UI Responsiveness)
  2. Windows Performance toolkit (Xperf)
  3. Lighthouse (접근성, 퍼포먼스, SEO)
  • 스크립트 실행 환경의 특징 ; 자바스크립트 성능 측면에서의 특징을 말한다.

    • 스크립트는 단일 스레드로 수행된다.
    • 렌더링과 스크립팅을 단일 이벤트 큐로 처리한다.
    • DOM 작업은 느리다.
    • 레이아웃 작업은 느리다.
    • Native code는 스크립트 코드보다 빠르다.
    • 스크립트 엔진은 코드를 최적화한다.

    단일 스레드 실행 환경을 도식화

  1. 웹페이지가 로드되면 '로드 이벤트'가 발생한다.
    2-1. '로드 이벤트 핸들러'가 실행된다.
    2-2. 여기서 '타이머 이벤트'가 발생한다고 가정해본다.

  2. '타이머 이벤트 핸들러'가 주기적으로 발생한다.

  3. 여기서 '클릭 이벤트'가 발생하고 무거운 처리가 이뤄졌다고 해보자.

  4. '타이머 이벤트'는 계속해서 발생하지만 '타이머 이벤트 핸들러'가 따라오지 않는다.
    이러면 브라우저가 멈춘 것처럼 보인다

  5. 클릭 이벤트 핸들러가 끝나면, 밀렸던 타이머 이벤트 핸들러가 주르륵 실행된다.

    단일 이벤트 큐를 도식화

    스크립팅과 렌더링을 같은 큐로 처리

  • 자바스크립트 이벤트 큐가 지연되면 화면을 못 그리는 상황이 발생한다.

    DOM 작업이 느린 이유

    브라우저 서브시스템 간의 호출을 유발
    기본적으로 리스트나 트리를 탐색하는 루프를 수행
    화면 갱신(reflow, repaint)을 유발 루프문에서는 DOM메소드 호출을 최소화할 필요가 있다.
    DOM을 조작하는 작업은 모아서 한 번에 처리해야 한다. Repaint와 Reflow?
    화면 갱신 작업의 두 가지 유형이다.
    repaint는 좌표, 크기 갱신이 없는 화면 갱신 작업이고
    reflow는 레이아웃 변동을 일으키는 작업이다.

시나리오 기반 자바스크립트 성능 최적화 사례

1. Number

64비트 부동소수점을 표현하는 'Wrapper Object' 자바스크립트에서 숫자를 이용하게 되면 반드시 사용되는 Wrapper Object는 기본적으로 64비트를 할당받는다. 심지어 오브젝트이기 때문에 스택이 아니라 힙에 들어가게 된다. 숫자 '1'만 넣고 빼도 생각보다 오버헤드가 크다.
브라우저는 숫자가 정수일 경우에 스택만 이용하도록 제한해주는 최적화를 지원한다.

      function doMath_1(){ // Slow coz it uses the heap memory.
          var a = 5;
          var b = 2;
          r = ((a + b)) / 2); // 3.5
      }
      var intR = Math.floor(r);
	  

      function doMath_2(){ // Recommended. Uses stack only.
          var a = 5;
          var b = 2;
          r = ((a + b)) / 2) | 0; // 3   Bitwise operation
          r = Math.round((a + b)) / 2); // 4
      }

2. Array

자바스크립트의 배열은 한 칸씩 초기화할 때마다 적절한 타입을 만들기 위해 배열 전체를 컨버팅한다. 0번째 칸에 정수가 들어오면 정수타입으로 했다가, 1번째 칸에 소수가 들어오면 실수타입, 문자가 들어오면 var타입으로 변환한다.

var a = new Array(10000); // Bad case
for(var i = 0; i < a.length; i++){
  a[i] = i;
}
a[9999] = "str"; 


var b = new Array(10000);
b[0] = "hint"; // The array type is correctly specified.
for(var i = 0; i < a.length; i++){
  b[i] = i;
}
b[9999] = "str";

3. Typed Array

굉장히 빠르다. 고정된 사이즈의 어레이를 할당하며, 타입이 미리 지정된다.

4. 배열 열거의 다양한 방법

a.length 꼴은 내부적으로 getter 메소드를 실행하기 때문에 느리다. 메소드를 호출하지 않고 변수만 참조하도록 최적화해야한다.
왼쪽은 ECMA표준이 정의하는 함수형 반복문과 더불어 일반적인 반복문을 설명하고 있다. 그러나 변수만 참조하도록 최적화한 오른쪽이 항상 좋은 퍼포먼스를 낸다. 약 10000번 루프테스트를 실시한 결과 오른쪽이 63.02배 빨랐다.

5. window.requestAnimationFrame()

브라우저가 매번 화면을 그리는데, '변화된 화면을 그릴 준비가 완료되었을 때' 즉시 그리도록 하는 최적화용 메소드. 브라우저는 초당 60번 반복을 목표로 작업을 수행한다. 일반적으로 재귀함수 형태다.

6. Do It Less

가장 빠른 코드는 실행하지 않는 코드다."

  • 불필요한 기능을 제거
    있어야 하는 코드가 아니라면 제거해야 한다.

  • 불필요한 단계 회피
    함수가 최종 결과를 얻기까지 필요한 단계가 아니라면 단순화해야 한다.

  • 스스로 질문해보자.
  1. 이 모듈이 정말로 필요한가?
    1. 이 프레임워크를 사용하는 이유가 뭘까?
      1. 오버헤드할 가치가 있나?
        1. 만약 그렇다면, 더 간단한 방법은 없을까?

6.1 Do It Less Often

코드를 제거할 수 없다면, 덜 실행할 수 있는지 확인한다.
반복문에서, 최종 결과를 얻기 위해 끝까지 반복할 필요가 없을 경우 break로 탈출해야 한다. 또한 loop 속에서 특정 요소에 대해서만 작업을 수행하면 되는 경우에 continue로 작업을 건너뛸 수 있도록 하자. 필요한 경우 label도 이용한다.

7. 연산 최소화

함수의 동작 순서를 신중하게 생각한다. 계산을 줄일 방법을 먼저 고민하고 코드화한다. 가장 비효율 적인 케이스로, 배열의 모든 항목에 대해 연산해버리는 경우를 들 수 있다.

8. Big O 표기법

메모리를 덜 차지하고 적게 계산하는 알고리즘을 구상하기 위한 기초지식이다.

9. Built-in Method로 연산

map(arr, el => el * 2);  // diy
arr.map(el => el * 2);   // built-in. 65% faster.

10. 메모리 신경쓰기

ES2017에 도입된 TypedArray 객체를 사용한다거나, WeakSet 및 WeakMap과 같은 약한 참조를 이용하여(참조되지 않은 값들이 가비지컬렉터에 수집되도록 함) 메모리 누수를 방지할 수 있다.

11. 변수 Polymorphism 피하기

자바스크립트 변수는 다형성을 갖는다. 반복해서 사용해야 하는 숫자가 아니라면 굳이 변수로 선언해서 쓰지 않는 편이 좋다.

let x = 2, y = 3;
multiply(x, y);

multiply(2, 3); // 1% faster.

1회 실행의 경우 1% 연산속도 이득을 볼 수 있다.

12. Do It Later

중요도가 높은 계산을 먼저 처리하도록 한다. 비동기(Asynchronous) 코드를 사용하면 어떤 작업에 대한 스레드 차단을 고려할 수 있게 된다.

13. Code Splitting

클라이언트측에서 js를 사용하는 경우 우선순위가 높은 내용은 가능한 빨리 표시해야 할 것이다.

javascript내의 code splitting 기능을 이용해 성능을 개선할 수 있다. 하나의 큰 번들로 js파일을 import 하는 대신, 필요한 최소 js 코드가 실행되도록 작은 코드로 분할할 수 있다. 코드 분할 방법은 React, Angular, Vue 혹은 Vanilla js 사용 여부에 따라 다를 수 있다.

관련 기술은 tree shaking을 참조하면 된다. 이 전략은 코드베이스에서 사용되지 않거나 불필요한 종속성을 제거하는데 중점을 둔다.

14. 스코프 줄이기

특정 함수를 호출할 때 마다, 해당 함수를 정의하는 데 사용되는 변수가 내부에 저장된다. 변수는 지역과 전역으로 구분된다. 함수 호출 중에, 컴파일러는 사용중인 변수의 스코프를 탐색한다. 스코프 수가 증가함에 따라, 현재 스코프 외부에 있는 변수에 접근하는데 걸리는 시간이 증가한다.

로컬 변수 접근 속도 > 글로벌 변수 접근 속도

15. js파일을 하나로 합치기

예를 들어, 앱에 7개의 JavaScript 파일이 있는 경우엔 브라우저가 7개의 서로 다른 HTTP 리퀘스트를 주고받아야 한다. 이러한 상황을 피하기위해서는 7개의 파일을 하나의 파일로 바꾸기만 해도 성능 향상을 기대해볼 수 있다.

16. 먼저 반복문을 줄이고, 반복문 안의 내용을 줄인다

당연한 얘기지만 종종 지키기 어렵다.

17. DOM 접근 최소화

자바스크립트의 네이티브 환경 외부에서 발생하는 모든 상호작용으로 인해 성능이 크게 저하되고 예측할 수 없게 된다. DOM 객체와 여러번 상호작용해야하는 경우, 브라우저는 매 번 새로고침을 해야하므로 성능이 저하된다. 이를 피하려면 DOM 접근을 최소화해야 한다. 예를 들어, 브라우저 객체에 대한 참조를 저장하거나, 전체 DOM 순회를 줄이는 방법이 있다.

18. 비동기 프로그래밍

대부분의 애플리케이션에서, 데이터를 가져오기위해 여러 API를 여러 번 호출하게 된다. 각 기능마다 별도의 미들웨어를 두는 식이다. 그러나, 자바스크립트는 싱글 스레드이며 수많은 동기적인 구성요소를 가지고 있다. 이러한 구성 요소들은 애플리케이션을 멈춘 것처럼 보이게 한다.

가능한 한 항상 코드에서 비동기 API를 사용해야한다. 비동기적인 코드를 작성할 땐, 초심자에게는 조금 어려울 수 있는 복잡성이 존재한다는 사실을 염두해두어야 한다.

19. 이벤트 위임

이벤트 위임을 사용해야 하나의 이벤트 핸들러를 사용하여 여러 이벤트를 효율적으로 처리할 수 있다. 이러한 방법은 결국 전체 페이지의 이벤트 유형을 효율적으로 관리하는 데 도움이 된다. 대규모 웹 애플리케이션의 경우에는 이벤트 위임을 사용하지 않는다면, 여러 이벤트 핸들러가 존재하게되어 성능을 저하시킨다.

이벤트 위임은 여러 장점을 가지고 있다.

  1. 관리할 기능이 적어진다
  2. 처리하는 데 필요한 메모리가 적어진다
  3. DOM을 다루는 코드가 적어진다.

20. Gzip 압축

Gzip은 대부분의 클라이언트와 서버에서 압축 및 압축 해제를위해 사용하는 소프트웨어 애플리케이션이다. gzip과 호환되는 브라우저가 자원을 요청하면 서버는 브라우저로 읍답을 보내기 전에 파일을 압축한다. Gzip은 큰 자바스크립트 파일을 압축하고 대역폭을 절약한다. 결과적으로 레이턴시와 시간 지연을 줄여주고 애플리케이션의 전체적인 성능을 향상시킨다.

21. 객체 캐싱

캐싱은 자주 접근하는 데이터를 캐시에 임시 저장하여 추가적인 요청에 재사용할 수 있도록 해준다.

22. 메모리 사용량 제한

메모리 사용량 제한은 자바스크립트 개발자가 가지고 있어야하는 주요 기술 중 하나다. 애플리케이션이 동작할 때에는, 디바이스에 필요한 정확한 메모리량을 결정하기 어렵기 때문이다.

애플리케이션이 브라우저를 위한 새로운 메모리 예약을 요청하면, 가비지 콜렉터가 시작되고 메모리를 확보하려고 시도한다. 자바스크립트 코드는 메모리가 있을때까지 기다려야한다. 이러한 문제가 계속되면, 페이지 속도가 느려지게됩니다.

23. 메모리 누수 모니터링

메모리 누수 문제를 분석하는데 크롬 개발자 도구의 성능(performance) 탭의 타임라인을 이용해보자. 일반적으로, 페이지에서 삭제된 DOM조각이 메모리 누수와 연관이 있다. 이러한 DOM조각들은 가비지 컬렉터가 동작하는 것을 방해하는 참조 변수를 가지고 있다.

profile
The interactive storytelling web service <Wishtree> is an open source project run by Team Noob. We share tips for effective non-face-to-face teamwork and service development process. Hope it helps a little to the world suffering from corona.

0개의 댓글