avatar
Published on

웹 페이지에서의 자바스크립트 메모리 사용량 벤치마킹

Author
  • avatar
    Name
    yceffort

메모리 사용량 분석은 웹과 관련된 이야기를 나눌 때 가장 어려운 주제 중 하나다.

사실 지금까지 실제 환경에서 페이지가 얼마나 많은 메모리를 사용하는지 정확하게 파악할 방법이 없었다. 즉, 메모리 사용량과 사용자 간의 상관관계를 끌어내 줄만한 적절한 지표가 존재하지 않았었다. 그래서 지금까지 메모리가 얼마나 문제가 되고 있는지 정확하게 알지 못했다.

웹 페이지의 메모리 사용량이 비즈니스에 미치는 영향에 대한 데이터도 없고, 벤치마킹을 위한 데이터도 없기 때문에 웹 개발 커뮤니티에서 메모리에 대한 관심이 별로 없었다. 또한 브라우저는 메모리와 관련된 지표를 측정할 인센티브가 별로 없었다.

닭이 먼저일까, 달걀이 먼저일까. 더 나은 툴이 없어서 분석을 안하는 걸까, 아님 분석을 할만한 유인이 없어서 안하는 걸까.🤔

먼저, 메모리 사용량이 비즈니스에 미치는 영향력을 잘 알지 못하기 때문에, 수많은 개별 사이트에서 먼저 메모리에 데이터가 어떻게 쌓이는지 추적을 하는 작업을 해야 한다.

measureUserAgentSpecificMemory

크롬은 measureUserAgentSpecificMemory라는 Performance Observer를 도입하여, 메모리 관련 정보를 수집하기 위한 API를 추가했다. measureUserAgentSpecificMemory는 자바스크립트와 DOM element와 관련된 메모리에 대해, 페이지가 현재 얼마나 사용하고 있는지 바이트 수에 대한 분석을 수행한다.

v8팀에 따르면, 웹 메모리의 35%는 자바스크립트, 10%는 DOM 을 그리기 위해 사용된다. 나머지 55%는 이미지, 브라우저 기능 및, 저장과 관련된 기능에서 사용된다. 이 API의 사용처가 자바스크립트와 DOM에 한정되어 있긴 하지만, 실제 페이지 메모리 사용량의 상당량 (45%)이 이 두개의 의해 사용되고 있다.

이 API를 사용하면, 다음과 같은 결과를 얻을 수 있게 된다.

// Console output:
{
  bytes: 60_000_000,
  breakdown: [
    {
      bytes: 40_000_000,
      attribution: [
        {
          url: "https://foo.com",
          scope: "Window",
        },
      ]
      types: ["JS"]
    },
    {
      bytes: 0,
      attribution: [],
      types: []
    },
    {
      bytes: 20_000_000,
      attribution: [
        {
          url: "https://foo.com/iframe",
          container: {
            id: "iframe-id-attribute",
            src: "redirect.html?target=iframe.html",
          },
        },
      ],
      types: ["JS"]
    },
  ]
}

최상단에는 자바스크립트와 DOM이 얼마나 메모리를 쓰고 있는지를 나타내는 bytes가 있고, breakdown을 살펴보면 실제로 얼마나 사용하고 있는지를 알 수 있다. 이 정보를 통해 페이지의 총 자바스크립트 및, DOM관련 메모리 사용량을 확인할 수 있을 뿐만 아니라, 다른 frame에서 사용하고 있는 메모리도 비교 분석할 수 있다.

테스트 준비하기

measureUserAgentSpecificMemoryPromise기반이기 때문에, 결과를 꽤 명확하게 알 수 있다.

if (performance.measureUserAgentSpecificMemory) {
  let result
  try {
    result = await performance.measureUserAgentSpecificMemory()
  } catch (error) {
    if (error instanceof DOMException && error.name === 'SecurityError') {
      console.log('The context is not secure.')
    } else {
      throw error
    }
  }
  console.log(result)
}

안타깝게도, 이 코드는 보안상의 문제로 인해 프로덕션에서 사용하는 것은 어렵다. 프로덕션에서 측정하기 위해서는, chrome에 --disable-web-security--no-site-isolation플래그를 전달해야 한다.

return new Promise((resolve) => {
  performance.measureUserAgentSpecificMemory().then((value) => {
    resolve(JSON.stringify(value))
  })
})

이 코드는 measureUserAgentSpecificMemory가 결과를 리턴할 때 까지 기다린 다음, (약 20초 정도) 전체 결과를 가져와 json으로 resolve한다.

몇 가지 벤치마크 정보를 가져오기 위해, 해당 지표와 chrome 플래그를 사용하여 테스트 한다음, 데스크톱 chrome 사용자 환경 보고서 인기 순위를 기준으로 한 top 1000개의 사이트를 기준으로 에뮬레이트된 Moto G4에서 테스트를 수행했다.

https://blog.webpagetest.org/posts/benchmarking-javascript-memory-usage/

얼마나 많은 메모리가 사용되고 있는가?

데스크톱 메모리 사용량 (kb)모바일 메모리 사용량 (kb)
10%2,391.7kb2,368.5kb
25%4,949.4kb4,784.3kb
50%10,236.6kb9,848.3kb
75%19,874.3kb19,033.0kb
90%31,814.5kb29,064.5kb
95%40,477.2kb37,546.1kb

대충 살펴보니, 중간값은 10MB정도를 사용했고, 75% 까지 정도만 가더라도 19.4mb까지 상승한다. 마지막 99%는, 조금 무서운 수치다.

앞선 맥락에서 설명을 이어가자면, 이 chrome 분석은 자바스크립트와 DOM에 국한되어 있으므로 45% 정도만을 차지하고 있다. 따라서 보통 웹 페이지가 22mb 정도의 메모리를 차지한다는 것을 알 수 있고, 최악의 경우에는 80MB까지도 차지할 수 있다. 이는 애플리케이션 창 내에 있는 단일 페이지의 메모리이다.

물론, 이 웹페이지가 무슨일을 하는지에 대한 이해가 없다면, 이게 많은 양을 차지하고 있다고 단정하기는 어렵다.

애초에 자바스크립트 관련 작업에 사용할 수 있는 메모리가 얼마나 되는지 조차도 불분명하다. 레거시 API (performance.memory)는 렌더러가 사용할 수 있는 자바스크립트 힙의 최대크기인 jsHeapSizeLimit을 제공하지만, 이러한 값은 브라우저마다 다르고 제대로 설정되어 있지 않기 때문에 이 값에 의존할 수는 없다.

그러나, 테스트한 결과를 대략적인 벤치마크로 사용할 수는 있다. 구글이 코어 웹 바이탈을 중심으로 정량화한 수치를 보면 아래와 같다.

데스크톱모바일
좋음< 4.8mb<4.7 mb
개선필요4.8mb <> 19.4mb4.7mb <> 18.6mb
나쁨>19.4mb>18.6mb

메모리와 다른 성능 지표간의 관계

앞서 이야기 했듯, 프로덕션 환경에서 메모리를 측정하는 것은 어려운 일이다. 데이터를 수집하기 위해서는 많은 보안 메커니즘이 필요하다. 이는 모든 사이트에서 의미있는 데이터를 얻기는 힘들다는 것을 의미한다. 성능 데이터를 비즈니스 및 전체 사용자 경험에서 미치는 영향간의 맥락에서 보는 것은 매우 어렵다.

대신 메모리 사용량이 다른 성능 지표와 어떤 상관관계가 있는지 확인해보는 것은 어떨까? 메모리가 자바스크립트와 DOM에 한정되어 있는 만큼, 페인트 관련 지표와 같은 전통적인 지표사이에 상관관계를 볼 수 있을 것이다. (1과 가까울 수록 메모리 사용량과 관계가 높다)

지표데스크톱 상관계수모바일 상관계수
렌더 시작.073.097
Largest Contentful Paint.168.218
Total Blocking Time.663.709
Load Time.365.463
자바스크립트 바이트.758.769
Dom Elements.216.234

자바스브립트를 많이 전송한다면, 결과적으로 많은 메모리를 사용하게 될 것이다. 또한 TBT(Total Blocking Time)이 길어지면, 많은 양의 자바스크립트 관련 메모리를 사용할 가능성이 높다.

https://res.cloudinary.com/psaulitis/image/upload/f_auto,q_auto,w_1800/js_mem_usage_attribution.png

프레임워크 별 메모리 사용량은 어떨까?

인기있는 유명한 프레임워크가 사용될 때, 메모리 사용량이 어떻게 될까? 여기서 주의사항은 메모리가 프레임워크 자체를 위한 모든 것이 아니라는 것이다. 여기에는 훨씬 더 많은 변수가 있다. 많은 프레임워크가 자체적으로 가상 DOM을 유지하므로 신중하게 살펴봐야 한다. 가상 DOM은 실제 DOM과 동기화하여 변경사항을 처리하는데 사용되는 일종의 메모리를 표현하는 인터페이스다.

따라서 자연스럽게 이 개념을 사용하는 프레임워크가 구축되면 메모리 사용량도 높아질 것이다.

https://res.cloudinary.com/psaulitis/image/upload/f_auto,q_auto,w_1800/js-mem-usage-framework.png

이 그래프를 봤을 때, 리액트가 메모리에 해롭다고 즉시 판단해 버릴 수 있는 위험이 존재한다. 메모리 사용량은 자바스크립트의 크기와 관계가 있고, 프레임워크를 사용하는 사이트가 자바스크립트 사이즈도 크다는 것을 우리는 모두 알고 있다. 따라서 메모리 사용량이 급격하게 증가하는 것은, 프레임워크가 비효율적이라는 게 아니고 리액트 사이트에서 코드를 많이 전송하는 경향이 있기 때문일 수도 있다.

프레임워크의 실제 메모리 효율성에 초점을 맞추는 것은 많은 노이즈가 존재할 수 있기 때문에 조금 어렵다. 메모리 효율성을 대략적으로 보여줄 수 있는 한가지 지표는 자바스크립트 바이트 대비 메모리 바이트 비율을 살펴보는 것이다. DOM은 메모리 사용량에 영향을 미치지만, DOM과 메모리 사용량의 약한 상관관계, 그리고 자바스크립트 바이트와 메모리 사용량간의 높은 상관관계를 고려할 때, 대략적이지만 유용한 벤치마크를 얻을 수도 있다. 비율이 낮을 수록 효율적이다. 즉, 자바스크립트 바이트당 메모리 바이트의 크기가 작다.

프레임워크자바스크립트 크기 대비 메모리 비율
평균5.3
Vue5.4
React4.7
jQuery5.5
Angular4.9

앞서 리액트에 대해 실망아닌 실망을 했다면(?) 이 결과는 놀랍다. 다른 프레임워크 대비 리액트의 비율이 낮은 것으로 나타났다. 물론 리액트는 Angular를 제외한 다른 프레임워크 대비 훨씬 더 많은 자바스크립트가 만들어져서 메모리 사용량이 커질 위험성이 존재한다. 항상 그렇듯이, 중요한 것은 무슨 프레임워크를 쓰던지 간에 자바스크립트의 총량을 적게 유지해야 한다.

여기에는 또다른 큰 주의사항이 있다. 이 데이터는 초기 페이지 로드에 따른 메모리 사용량이다. 이 자체로도 흥미롭지만, 잠재적인 메모리 누수에 대해서는 알 수가 없다. 메모리 누수는 SPA에서 매우 흔하다. 이는 다른 글에서 다뤄야 한다.

마치며

메모리는 여전히 웹 성능을 분석하는 데 있어 미지의 영역이지만, 이는 바뀔 필요가 있다. 계속해서 자바스크립트의 크기가 증가함에 따라, 메모리 사용량도 커질 수 있다.

전체적인 그림을 그려보기 위해서는 더 많은 정보가 필요하다. 브라우저에서 실제로 사용할 수 있는 메모리는 어느정도일까? 메모리는 비즈니스와 사용자간 지표와 어떤 관계가 있을까? 자바스크립트와 DOM의 복잡성과는 관계가 없는 메모리 사용량은?

실제 사용자 데이터를 모니터링하여 사이트에 이 데이터를 가져오는 것에는 어려움이 있지만, 여기에서 사용한 방식 (flag등)을 활용하여 메모리 관련 테스트 결과를 가져올 수도 있다. 이러한 작업을 통해 더 많은 정보를 파해쳐보자.