Trello의 boards 로딩시간을 단축하기위해 1주일동안 한 작업들
문제
- 1000건의 카드를 읽으려면 렌더링 완료에 7초 이상 걸렸다.
목표
- 1400x1000 윈도우의 906개의 카드 로딩 시간을 하루에 10% 씩 단축하기 위해 노력한 기록.
월요일 7.2초 -> 6.7초 7% 감소
- border, shadow, gradient 와 같은 무거운 CSS 스타일들은 브라우저를 굉장히 느리게 만든다.
- 그래서 이런것들을 제거하기 위해 노력했다.
- 이런 작업은 특히 스크롤 성능에 효과가 있었다.
- 우리는 플랫 디자인을 위해서 작업한게 아니라 속도를 위해서 작업했는데
- 더 깔끔해지고 단순해졌다.
화요일 6.7초 -> 5.9 12% 감소
- 우리는 Backbone.js 을 썼다.
- Backbone.js 는 view를 사용하기 아주 쉽게 만든다.
- 모든 카드의 멤버 각각에 view 를 생성했다.
- 만약 멤버를 클릭하면 프로필과 메뉴를 볼 수 있었다.
- view가 불필요하게 많이 생성되서 브라우저가 시간을 쓰게 된다.
- 그래서 click handler 를 만들고 필요할때만 view를 생성하도록 바꿨다.
- 추가적으로 CSS도 줄였다.
수요일 5.9초 -> 5.9초 0% 감소
- jQuery 대신에 브라우저 네이티브 innerHTML과 getElementByClassName 메소드를 쓰려고 노력했다.
- 네이티브 API를 사용하면 성능이 개선될줄 알았지만 효과를 보지 못했다.
- 쓸데없는 시간을 보냈다..
목요일 5.9초 -> 960 ms
- 목요일은 정말 획기적인 성과가 있었다.
- 2가지를 했다. “preventing layout thrashing”, “progressive rendering”
Preventing layout thrashing
- layout thrashing
- 브라우저는 HTML을 렌더링할때 크게 두 가지를 한다.
- layout: element 의 크기와 위치 계산(결정)하는 것.
- paint: 올바른 위치에 정확한 color로 픽셀을 그리는 것.
- borders, backgrounds 등의 CSS를 줄이는 것으로 paint 를 줄였지만 여전히 layout 에는 이슈가 있다.
- 브라우저는 HTML을 렌더링할때 크게 두 가지를 한다.
- 카드를 렌더링 하는 순서:
- 하얀색 카드 프레임과 빈 카드명이 DOM에 삽입된다.
- 그리고 label, 멤버 뱃지 등이 추가 된다.
- 이렇게 한 이유:
- 실시간으로 업데이트 하기 위해
- 뭔가 바뀌면 자동적으로 카드의 그 부분이 렌더링 되어야 한다.
- 예를 들어, 멤버가 추가되면 cardView.renderMembers 메소드가 트리거 되어 이 멤버만 렌더링 된다.
- 카드 전체를 다시 렌더링 하여 깜박거리는걸 피하기 위해서 이렇게 한다.
- HTML 을 미리 만들고, DOM을 삽입하고, 그러면 layout 이 실행된다.
- HTML을 좀더 만들면 DOM이 좀더 삽입되고, layout 이 좀 더 실행된다.
- 카드에 여러번 DOM을 삽입하기 때문에 대량의 layout이 실행된다.
- 해결책: 렌더링을 카드를 DOM에 삽입하기 전에 수행하는 형태로 변경했다.
원래 카드 view 를 렌더링하는 함수는 아래와 같았는데
render: ->
data = model.toJSON()
@$.innerHTML = templates.fill(
'card_in_list',
data
) # add stuff to the DOM, layout
@renderMembers() # add more stuff to the DOM, layout
@renderLabels() # add even more stuff to the DOM, layout
@
아래와 같이 바꿨다.
render: ->
data = model.toJSON()
data.memberData = []
for member in members
memberData.push member.toJSON()
data.labelData = []
for labels in labels when label.isActive
labelData.push label
partials =
"member": templates.member
"label": templates.label
@$.innerHTML = templates.fill(
'card_in_list',
data,
partials
) # only add stuff to the DOM once, only one layout
@
- layout 문제는 아직 좀더 있다.
- 과거에는 리스트의 width 가 스크린 크기 별로 조정되고 있었기 때문에
- 사이드바를 만지거나, 목록을 추가하너가, 창 크기를 변경하거나 하는 등의 작업을 할때마다 layout 이 일어났다.
- 그래서 list 의 폭을 고정하는 것으로 layout 을 하지 않도록 했다.
- 이것 때문에 조정을 맘대로 할 수 없게 되었지만 이건 뭐 성능을 위한 trade-off 다.
Progressive rendering
- 하지만 여전히 브라우저는 5초가 걸린다.
- Chrome 의 Timeline 을 보면 대부분 script 에서 시간을 쓰고 있었다.
- Trello 개발자 Brett Kiefer는 예전에 jQuery UI droppables 의 초기화를 비동기 라이브러리의 큐 메소드를 사용해서 board 를 paint 한 이후로 defferring(연기)하는 것으로 UI가 잠기는걸 고쳤었다.
- click -> 긴 작업 -> paint
- 를
- click -> paint -> 긴 작업
- 으로 바꾼 것이다.
- 비슷한 기술로 카드 렌더링에 응용할 수 없을까 생각했다.
- 큰 DOM을 생성하여 삽입하는데 시간을 쓰는게 아니라
- 작은 DOM을 생성 -> 삽입 -> 다음 DOM 생성 -> 삽입 (반복)
- 이렇게 해서 브라우저의 UI스레드를 free up 하고
- 빠르게 paint 하고
- prevent locking up (잠겨지는걸 피할 수 있다.)
- 이 작업으로 한번에 960ms 까지 단축할 수 있었다.
- 이 과정을 요약하면 아래와 같다.
- 코드는 아래와 같다.
- 리스트 안에 있는 카드들은 Backbone.js의 collection 안에 있다.
- collection은 자신의 view가 있다.
큐 기법을 이용한 card collection view의 render 메소드는 러프하게는 아래 코드와 같다.
render: ->
renderQueue = new async.queue (models, next) =>
@appendSubviews(@subview(CardView, model) for model in models)
# _.defer, a.k.a. setTimeout(fn, 0), will yield the UI thread
# so the browser can paint.
_.defer next
, 1
chunkSize = 30
models = @getModels()
modelChunks = []
while models.length > 0
modelChunks.push(models.splice(0, chunkSize))
for models in modelChunks
# async.queue flattens arrays so lets wrap this array
# so it’s an array on the other end...
renderQueue.push [models]
@
- 또
translateZ: 0
라는 hack 을 적용했다.- covers, stickers, and member avatars 등 이미지 데이터가 많은데.
- 이걸 적용하면 브라우저는 paint 하는데 GPU를 쓰기 때문에 GPU가 처리하는 동안 다른 일을 할 수 있다.
금요일
- 이번주에 발생한 버그들을 수정했다.