이전 글 "Metro 스타일 앱의 성능을 향상시키는 방법"에서는 빠르고 유연한 앱을 만드는 방법과 필요한 도구에 대해 살펴보았습니다. 이제 앱의 성능을 저하시키는 일반적인 요소에 대해 자세히 알아보겠습니다. 이 글에서는 제가 경험으로 얻은 교훈을 바탕으로, 개발자가 JavaScript와 XAML 모두를 사용하여 Metro 스타일 앱을 개발할 때 성능 측정이 가능하도록 하고 성능을 현저하게 개선할 수 있는 방법을 소개하겠습니다. 또한 사용하는 언어에 관계없이 성능을 대폭 높일 수 있는 5가지 구체적인 사례를 설명하겠습니다. 이러한 사례를 적용하는 데에는 어떤 교묘한 기술이나 복잡한 작업이 필요한 것은 아닙니다. 여러분이 다음 지침을 따른다면 앱의 성능을 상당히 개선할 수 있을 것으로 확신합니다. 아울러 이러한 지침이 얼마나 도움이 되었는지를 저희에게 알려 주시고 여러분이 알고 있는 유용한 정보를 함께 공유해 주시면 감사하겠습니다.

일반 지침

네트워크 콘텐츠보다는 패키지로 제공되는 콘텐츠를 신뢰합니다.

  • 로컬 이미지 및 파일은 항상 네트워크를 통해 제공되는 것보다 빨리 검색됩니다.
  • 앱에서 "라이브" 이미지를 로드해야 하는 경우 해당 이미지를 검색하는 동안 로컬 이미지를 자리 표시자로 사용하는 것이 가장 좋습니다.

로컬 이미지의 배율을 올바른 크기로 지정합니다.

  • 이미지가 항상 같은 해상도로 표시되는 경우 배율이 미리 지정된 해당 해상도로 이미지를 패키징합니다. 그러면 이미지가 표시될 때마다 실시간으로 배율이 지정되는 것이 방지되므로 앱의 수명 주기 동안 성능이 거의 저하되지 않습니다.
  • 이미지가 여러 해상도로 표시될 수 있는 경우에는 특별한 이유가 없는 한 여러 버전의 이미지를 패키징합니다.

최신 앱의 실행 시간을 빠르게 유지합니다.

  • 시작 화면이 사라진 후에는 네트워크 작업만 수행합니다.
  • 앱이 활성화되는 동안에는 데이터베이스 및 기타 대용량 메모리 내 개체의 로드를 나중으로 미룹니다.
  • 완료할 대규모 작업이 있는 경우 앱이 이러한 작업을 백그라운드에서 수행할 수 있도록 사용자 지정 시작 화면 또는 축소된 방문 페이지를 제공합니다.

Windows 8은 다양한 장치에서 실행되므로 사용자의 해상도에 적합한 미디어 콘텐츠를 사용합니다.

  • 사용자의 해상도에 비해 너무 작은 콘텐츠를 로드하면 화질이 떨어집니다.
  • 사용자의 해상도에 비해 너무 큰 콘텐츠를 로드하면 시스템 리소스에 불필요한 부하가 걸립니다.

앱의 응답성을 강조합니다.

  • 동기 API로 UI 스레드를 차단하지 마십시오. 비동기 API를 사용하거나 비차단 컨텍스트(예: 다른 스레드에 의한 차단)에서 동기 API를 호출합니다.
  • 시간이 오래 걸리는 계산은 UI 스레드가 아닌 스레드로 이동합니다. 이는 100밀리초 이상의 지연은 사용자가 감지할 수 있기 때문에 중요합니다.
  • 시간 소모적인 작업은 UI 스레드가 중간에 사용자 입력을 수신할 수 있도록 보다 작은 청크로 나눕니다.
  • 웹 작업자/스레드를 사용하여 메모리 사용량이 많은 작업을 오프로드합니다.
  • 새로 고칠 때보다 더 빠르게 화면으로 이동하지 마십시오. 입력 이벤트는 화면 새로 고침 속도보다 훨씬 빠르게 실행됩니다. 따라서 이러한 이벤트를 사용하여 화면을 업데이트하면 불필요한 작업이 많이 발생합니다. 대신 화면 이동을 화면 새로 고침 속도로 동기화하십시오.

JavaScript를 사용하는 Metro 스타일 앱의 성능 조정

JavaScript로 Metro 스타일 앱을 코딩할 경우 웹 사이트에서 JavaScript를 코딩할 때 여러분이 사용한 모든 유용한 정보를 적용할 수 있습니다. 그러나 우리가 조사하고 관찰한 바에 따르면, JavaScript로 작성된 Metro 스타일 앱의 경우 가장 강력한 성능 이득을 얻을 수 있는 세 가지 주요 방법이 있습니다. 자세한 내용은 개발자 센터에서 JavaScript를 사용하는 Metro 스타일 앱의 성능 모범 사례를 참조하십시오.

빠른 렌더링을 위해 축소판 사용

파일 시스템과 미디어 파일은 대부분의 앱에서 중요한 요소인 동시에 성능에 문제를 일으키는 가장 대표적인 원인 중 하나입니다. 미디어 파일을 액세스할 때 그 속도가 느릴 수 있는데, 이는 미디어를 저장 및 디코딩하거나 미디어를 표시하는 데 메모리 및 CPU 주기를 사용하기 때문입니다.

대용량 버전의 이미지를 축소판으로 표시하기 위해 배율을 낮추는 대신 Windows 런타임 축소판 API를 사용하십시오. 전체 크기 이미지의 배율을 낮추면 앱에서 전체 크기 이미지를 읽고 디코딩한 다음 배율을 조정하기 위한 추가 시간이 필요하므로 효율적이지 못합니다. Windows 런타임은 앱에서 축소판에 사용할 작은 버전의 이미지를 신속하게 가져올 수 있도록 효율적인 캐시로 지원되는 API 집합을 제공합니다.

아래 예에서는 사용자에게 이미지에 대한 메시지를 표시한 다음 이미지를 표시합니다.

// Pick an image file
picker.pickSingleFileAsync()
.then(function (file) {
var imgTag = document.getElementById("imageTag");
imgTag.src = URL.createObjectURL(file, false);
});

이 예는 이미지를 전체 크기 또는 거의 전체 크기로 렌더링하려는 경우에 적합하지만 이미지의 축소판 보기를 표시하는 데에는 비효율적입니다. 축소판 API는 앱에서 전체 크기 이미지보다 훨씬 빠르게 디코딩 및 표시할 수 있는 이미지의 축소판 버전을 반환합니다. 다음 예에서는 getThumbnailAsync 메서드를 사용하여 이미지를 검색하고 해당 이미지를 기반으로 축소판을 만듭니다.

// Pick an image file
picker.pickSingleFileAsync()
.then(function (file) {
var properties = Windows.Storage.FileProperties.ThumbnailMode;
return file.getThumbnailAsync(properties.singleItem, 1024);
})
.then(function (thumb) {
var imgTag = document.getElementById("imageTag");
imgTag.src = URL.createObjectURL(thumb, false);
});

측정한 결과, 이 패턴은 이미지 로드 시간을 1000%나 향상시킬 수 있는 것으로 나타났습니다. 즉, 축소판 API를 사용할 경우 디스크에서 직접 액세스하여 배율을 조정할 때보다 이미지가 최대 10배 빠르게 로드됩니다.

DOM 상호 작용을 최소로 유지

JavaScript를 사용하는 Metro 스타일 앱의 플랫폼에서는 DOM과 JavaScript 엔진이 별개의 구성 요소입니다. 이러한 구성 요소 간의 통신이 포함된 모든 JavaScript 작업은 순전히 JavaScript 런타임 내에서 수행할 수 있는 작업에 비해 비교적 메모리를 많이 사용합니다. 따라서 이러한 구성 요소 간의 상호 작용을 최소한으로 유지하는 것이 중요합니다. 예를 들어 DOM 요소의 속성을 가져오거나 설정하는 작업에는 상당히 많은 메모리가 사용될 수 있습니다. 아래 예에서는 body 속성 및 기타 여러 속성에 반복적으로 액세스합니다.

// Don’t use: inefficient code. 
function calculateSum() {
// Retrieve Values
var lSide = document.body.all.lSide.value;
var rSide = document.body.all.rSide.value;

// Generate Result
document.body.all.result.value = lSide + rSide;
}

function updateResultsStyle() {
if (document.body.all.result.value > 10) {
document.body.resultsDiv.class = "highlighted";
} else {
document.body.resultsDiv.class = "normal";
}
}

다음 예는 DOM 속성에 반복적으로 액세스하는 대신 DOM 속성 값을 캐시하여 코드를 향상시킵니다.

function calculateSum() {
// Retrieve Values
var all = document.body.all;
var lSide = all.lSide.value;
var rSide = all.rSide.value;

// Generate Result
all.result.value = lSide + rSide;
}

function updateResultsStyle() {
var body = document.body;
var all = body.all;
if (all.result.value > 10) {
body.resultsDiv.class = "highlighted";
} else {
body.resultsDiv.class = "normal";
}
}

DOM에서 요소를 배치하거나 그리는 방법에 직접적으로 영향을 주는 정보를 저장할 때는 DOM 개체만 사용하십시오. 이전 예의 'lSide''rSide' 속성이 앱의 내부 상태에 대한 정보만 저장하는 경우에는 이를 DOM 개체에 연결하지 마십시오. 다음 예에서는 JavaScript 개체만 사용하여 앱의 내부 상태를 저장합니다. 이 예에서는 화면을 업데이트해야 하는 경우에만 DOM 요소를 업데이트합니다.

var state = {
lValue: 0,
rValue: 0,
result: 0
};

function calculateSum() {
state.result = lValue + rValue;
}

function updateResultsStyle() {
var body = document.body;
if (result > 10) {
body.resultsDiv.class = "highlighted";
} else {
body.resultsDiv.class = "normal";
}
}

측정한 결과, 단순히 DOM에서 데이터에 액세스할 경우 DOM에 연결되지 않은 변수에 액세스할 때에 비해 액세스 시간이 최대 700% 증가할 수 있는 것으로 나타났습니다.

효율적인 레이아웃 관리

화면에서 앱을 렌더링하려면 시스템에서 HTML, CSS 및 기타 사양의 규칙을 DOM 요소의 크기 및 위치에 적용하는 복잡한 처리 작업을 수행해야 합니다. '레이아웃 단계'라고 하는 이 프로세스에는 상당히 많은 메모리가 사용될 수 있습니다.

레이아웃 단계를 트리거하는 API의 예를 들면 다음과 같습니다.

  • window.getComputedStyle
  • offsetHeight
  • offsetWidth
  • scrollLeft
  • scrollTop

레이아웃 단계는 레이아웃 정보가 마지막으로 수집된 이후에 레이아웃에 영향을 주는 요소가 변경된 경우 이러한 API를 호출하는 과정에서 발생합니다. 레이아웃 단계 수를 줄이는 한 가지 방법은 레이아웃 단계를 트리거하는 API 호출을 일괄 처리하는 것입니다. 이 작업 방법은 다음 코드 조각을 참조하십시오. 여기의 두 예에서는 모두 요소의 offsetHeightoffsetWidth를 5로 조정합니다. 먼저 offsetHeightoffsetWidth를 조정하는 데 사용되는 일반적이지만 비효율적인 방법부터 살펴보겠습니다.

// Don't use: inefficient code.
function updatePosition(){
// Calculate the layout of this element and retrieve its offsetHeight
var oH = e.offsetHeight;

// Set this element's offsetHeight to a new value
e.offsetHeight = oH + 5;

// Calculate the layout of this element again because it was changed by the
// previous line, and then retrieve its offsetWidth
var oW = e.offsetWidth;

// Set this element's offsetWidth to a new value
e.offsetWidth = oW + 5;
}

// At some later point the Web platform calculates layout again to take this change into account and render
the element to the screen

위 예에서는 세 개의 레이아웃 단계를 트리거합니다. 이제 같은 결과를 얻을 수 있는 더 나은 방법을 살펴보겠습니다.

function updatePosition() {
// Calculate the layout of this element and retrieve its offsetHeight
var height = e.offsetHeight + 5;

// Because the previous line already did the layout calculation and no fields were changed, this line retrieves
the offsetWidth from the previous line

var width = e.offsetWidth + 5;

//set this element's offsetWidth to a new value
e.offsetWidth = height;

//set this element's offsetHeight to a new value
e.offsetHeight = width;
}

// At some later point the system calculates layout again to take this change into account and render element to the screen

두 번째 예는 첫 번째 예와 별반 차이점이 없어 보이지만 레이아웃 단계를 세 개가 아닌 두 개를 트리거하므로 33% 향상된 효과를 제공합니다. 성능에 미치는 영향은 DOM 및 관련 스타일의 크기와 복잡성에 따라 차이가 있습니다. 앱의 UI가 풍부할수록 이 지침을 따르는 것이 더욱 중요합니다.

XAML로 작성된 Metro 스타일 앱의 성능 조정

체계화된 XAML은 활성화 시간, 페이지 탐색 및 메모리 사용량 등의 많은 주요 시나리오에서 이점을 제공합니다. 다음은 앱의 XAML을 조정하는 데 유용한 몇 가지 팁입니다.

중복 방지

요소 트리가 깊은 복잡한 UI에서는 XAML 구문을 분석하여 메모리 내에서 해당 개체를 만드는 데 시간이 오래 걸릴 수 있습니다. 따라서 시작 프로세스가 끝난 후에 가져와야 하는 XAML만 로드하는 것이 좋습니다. 가장 간편한 방법은 첫 번째 시각적 요소를 표시하는 데 필요한 페이지만 로드하는 것입니다. 첫 번째 페이지의 XAML을 자세히 살펴보고 필요한 모든 요소가 포함되어 있는지 확인하십시오. 다른 곳에 정의된 컨트롤 또는 스타일을 참조하는 경우에는 프레임워크에서 해당 파일도 구문 분석합니다.

<!--This is the first page an app displays. A resource used by this page, TextColor, is defined in 
AppStyles.xaml which means that file must be parsed when this page is loaded. Because AppStyles.xaml
contains many app-wide resources, all of these resources need to be parsed even though they aren’t
necessary to start the app. -->

<Page ...>
<Grid>
<TextBox Foreground="{StaticResource TextColor}" />
</Grid>
</Page>

Contents of AppStyles.xaml
<ResourceDictionary>
<SolidColorBrush x:Key="TextColor" Color="#FF3F42CC"/>

<!--many other resources used across the app and not necessarily for startup.-->
</ResourceDictionary>

리소스 사전을 자르십시오. 스토어 앱 범위의 리소스를 응용 프로그램 개체에 저장하여 중복을 피할 수 있지만 단일 페이지에 특정한 리소스를 해당 페이지의 리소스 사전으로 이동하십시오. 그러면 앱이 시작될 때 구문 분석되는 XAML의 양이 줄어들어 사용자가 특정 페이지로 이동할 때 해당 XAML을 구문 분석하는 리소스만 발생하게 됩니다.

<!--Bad: XAML which is specific to a page should not be included in the App’s resource
dictionary. The app incurs the cost of parsing resources that are not immediately
necessary at startup, instead of parsing on demand. These page specific resources should be
moved to the resource dictionary for that page.-->
<Application>
<Application.Resources>
<SolidColorBrush x:Key="DefaultAppTextColor" Color="#FF3F42CC"/>
<SolidColorBrush x:Key="HomePageTextColor" Color="#FF3F42CC"/>
<SolidColorBrush x:Key="SecondPageTextColor" Color="#FF3F42CC"/>
<SolidColorBrush x:Key="ThirdPageTextColor" Color="#FF3F42CC"/>
</Application.Resources>
</Application>

XAML for the home page of the app
<Page ...>
<StackPanel>
<TextBox Foreground="{StaticResource HomePageTextColor}" />
</StackPanel>
</Page>

XAML for the second page of the app
<Page ...>
<StackPanel>
<Button Content="Submit" Foreground="{StaticResource SecondPageColor}" />
</StackPanel>
</Page>
 

<!--Good: All page specific XAML has been moved to the resource dictionary for the
page on which it’s used. The home page specific XAML was not moved because it must
be parsed at start up anyway. Moving it to the resource dictionary of the home page
wouldn’t be bad either.-->
<Application>
<Application.Resources>
<SolidColorBrush x:Key="DefaultAppTextColor" Color="#FF3F42CC"/>
<SolidColorBrush x:Key="HomePageTextColor" Color="#FF3F42CC"/>
</Application.Resources>
</Application>

XAML for the home page of the app
<Page ...>
<StackPanel>
<TextBox Foreground="{StaticResource HomePageTextColor}" />
</StackPanel>
</Page>

XAML for the second page of the app
<Page ...>
<Page.Resources>
<SolidColorBrush x:Key="SecondPageTextColor" Color="#FF3F42CC"/>
</Page.Resources>

<StackPanel>
<Button Content="Submit" Foreground="{StaticResource SecondPageTextColor}" />
</StackPanel>
</Page>

요소 수 최적화

XAML 프레임워크는 수천 개의 개체를 표시하도록 설계되었지만 페이지의 요소 수를 줄이면 앱 레이아웃 및 장면 렌더링이 훨씬 빨라집니다. 시각적 복잡성을 같은 수준으로 유지하면서 장면의 요소 수를 줄일 수 있는 몇 가지 요령이 있습니다.

  • 불필요한 요소를 방지하십시오. 예를 들어 패널의 Background 속성을 설정할 때는 해당 패널의 요소 뒤에 색상이 지정된 사각형을 배치하는 대신 배경색을 제공하도록 설정합니다.
<!--Bad XAML uses an unnecessary Rectangle to give the Grid a black background-->                                                                
<Grid>
<Rectangle Fill="Black"/>
</Grid>
 
<!--Good XAML uses the Grid’s background property-->
<Grid Background="Black" />
  • 차단되거나 투명하여 표시되지 않는 요소를 축소하십시오.

  • 동일한 벡터 기반 요소가 여러 번 재사용되는 경우 이를 이미지로 만드는 것이 좋습니다. 이미지의 메모리는 이미지 URI당 한 번만 할당되기 때문입니다. 반면, 벡터 기반 요소는 인스턴스마다 인스턴스화됩니다.

독립 애니메이션 사용

애니메이션을 만들 때 처음부터 끝까지 계산될 수 있습니다. 하지만 애니메이션된 속성의 변경 내용이 장면의 나머지 개체에 영향을 주지 않는 경우가 있습니다. 이를 독립 애니메이션이라고 하며, UI 스레드 대신 컴퍼지션 스레드에서 실행됩니다. 컴퍼지션 스레드는 일정한 주기로 업데이트되기 때문에 독립 애니메이션은 항상 원활하게 재생됩니다. 다음 항목은 모두 독립적으로 유지됩니다.

  • 키 프레임을 사용하는 개체 애니메이션
  • 기간이 0인 애니메이션
  • Canvas.Left/Top
  • UIElement.Opacity
  • SolidColorBrush.Color(여러 속성에 적용된 경우)
  • 다음의 하위 속성
    • UIElement.RenderTransform
    • UIElement.Projection
    • UIElement.Clip의 RectangleGeometry

종속 애니메이션은 레이아웃에 영향을 주지만 UI 스레드의 추가 입력 없이는 계산할 수 없습니다. 이러한 애니메이션에는 너비 및 높이와 같은 속성에 대한 수정 사항이 포함됩니다. 기본적으로 종속 애니메이션은 실행되지 않으며 앱 개발자의 옵트인을 필요로 합니다. 사용하도록 설정된 경우 종속 애니메이션은 UI 스레드가 차단되지 않은 상태로 유지될 때 원활하게 실행되지만 프레임워크 또는 앱에서 다른 많은 작업을 수행할 때는 딜레이되기 시작합니다.

XAML 프레임워크는 기본적으로 거의 모든 애니메이션을 독립적으로 만들기 위해 노력해 왔지만 이 최적화를 사용하지 않기 위해 수행할 수 있는 몇 가지 작업이 있습니다. 다음 작업을 수행할 때는 매우 주의해야 합니다.

  • EnableDependentAnimation 속성을 설정하면 종속 애니메이션을 UI 스레드에서 실행할 수 있습니다. 이러한 애니메이션을 독립 버전으로 변환합니다. 예를 들어 개체의 WidthHeight 대신 ScaleTransform.XScaleScaleTransform.YScale을 애니메이션합니다.
  • 효과적인 종속 애니메이션인 프레임별 업데이트를 지정합니다. 예를 들어 핸들 변환을 CompositonTarget.Rendering에 적용합니다.
  • CacheMode=BitmapCache인 요소에서는 모든 애니메이션이 독립적으로 실행되는 것으로 간주됩니다. 애니메이션 실행이 종속적으로 간주되는 것은 캐시에서 각 프레임을 다시 래스터화해야 하기 때문입니다.

내용이 풍부하고 복잡하면서도 빠르고 유연한 앱을 만드는 것은 예술과도 같습니다. 저는 여러분이 고려해야 할 몇 가지 구체적인 사항을 소개했지만 하나의 도구나 한 가지 방법만으로 뛰어난 성능을 구현할 수 있는 경우는 거의 없습니다. 디자인 초기 단계에서 성능을 고려하는 여러 가지 연습의 축적이 성능을 결정하는 경우가 많습니다. 이 글이 여러분이 고객의 높은 기대를 충족시키는 앱을 만드는 데 도움이 되기를 바랍니다.

또한 코딩 작업이 보다 즐겁고 능률적으로 이뤄지기를 바랍니다.

- Windows 프로그램 관리자, Dave Tepper