應用程式啟動效能的最佳做法

透過改善處理啟動和啟用的方式,建立具有最佳啟動時間的通用 Windows 平台 (UWP) 應用程式。

應用程式啟動效能的最佳做法

在某種程度上,使用者會根據啟動需要多久時間來感覺您的應用程式速度為快速或緩慢。 針對本主題的目的,應用程式的啟動時間開始於使用者啟動應用程式時,並在使用者能以某種有意義的方式與應用程式互動時結束。 本節提供有關如何在應用程式啟動時取得更佳效能的建議。

測量應用程式的啟動時間

請務必先啟動應用程式幾次,再實際測量其啟動時間。 這可提供測量的基準,確保您盡可能縮短啟動時間。

在您 UWP 應用程式抵達客戶的電腦上時,您的應用程式已使用 .NET Native 工具鏈進行編譯。 .NET Native 是預先編譯技術,可將 MSIL 轉換成原生執行的機器程式碼。 .NET 原生應用程式啟動速度較快、使用較少的記憶體,且使用比 MSIL 對應項目更少的電力。 以 .NET Native 建置的應用程式會在自訂執行階段和可在所有裝置上執行的新聚合式 .NET Core 靜態連結,因此它們不會相依於內建的 .NET 實作。 在您的開發電腦上,如果您的應用程式是以「發行」模式建置,則預設會使用 .NET Native,而如果您在「偵錯」模式中建置,則會使用 CoreCLR。 您可以從 [建置] 頁面的 [屬性](C#) 或 [我的專案] 中的 [編譯->進階](VB) 在 Visual Studio 中設定此專案。 尋找顯示 [使用 .NET 原生工具鏈編譯] 的核取方塊。

當然,您應該測量能代表終端使用者體驗的內容。 因此,如果您不確定要將應用程式編譯為開發電腦上的機器碼,您可以執行原生映像產生器 (Ngen.exe) 工具來預先編譯您的應用程式,再測量其啟動時間。

下列程序說明如何執行 Ngen.exe 來預先編譯您的應用程式。

執行 Ngen.exe

  1. 至少執行您的應用程式一次,以確保 Ngen.exe 能偵測到它。

  2. 執行下列其中一項動作,開啟 [工作排程器]

    • 從開始畫面搜尋「工作排程器」。
    • 執行「taskschd.msc」。
  3. 在 [工作排程器] 的左側窗格中,展開 [工作排程器程式庫]

  4. 展開 [Microsoft]

  5. 展開 [Windows]

  6. 選取 [.NET Framework]

  7. 從工作清單中選取 [.NET Framework NGEN 4.x]

    如果您使用 64 位元電腦,也有 [.NET Framework NGEN v4.x 64]。 如果您要建置 64 位元應用程式,請選取 [.NET Framework NGEN v4.x 64]

  8. 在 [動作] 功能表上按一下 [執行]

Ngen.exe 會先行編譯電腦上已使用且沒有原生映像的所有應用程式。 如果有許多應用程式需要先行編譯,這可能需要很長的時間,但後續的執行速度會快得多。

當您重新編譯應用程式時,不會再使用原生映像。 相反地,應用程式會進行即時編譯,這表示它會在應用程式執行時進行編譯。 您必須重新執行 Ngen.exe,才能取得新的原生映像。

盡可能延遲工作

為了改善應用程式的啟動時間,請只做絕對必須做的工作,讓使用者開始與應用程式互動。 如果您可以延遲載入其他組件,這特別有用。 Common Language Runtime 會在第一次使用組件時載入組件。 如果您可以將載入的組件數目降到最低,您可以改善應用程式的啟動時間和記憶體耗用量。

獨立執行長時間執行的工作

即使應用程式的某些部分無法完全正常運作,您的應用程式還是可以互動。 例如,如果您的應用程式顯示需要一段時間才能擷取的資料,您可以藉由以非同步方式擷取資料,讓該程式碼獨立於應用程式的啟動程式碼執行。 當資料可供使用時,請使用資料填入應用程式的使用者介面。

許多擷取資料的通用 Windows 平台 (UWP) API 都是非同步,因此您可能還是會以非同步方式擷取資料。 如需非同步 API 的詳細資訊,請參閱在 C# 或 Visual Basic 中呼叫非同步 API。 如果您執行的工作未使用非同步 API,您可以使用 Task 類別來執行長時間執行的工作,以免妨礙使用者與應用程式互動。 這會讓應用程式在資料載入時回應使用者。

如果您的應用程式需要很長的時間才能載入部分 UI,請考慮在該區域中新增字串,其內容如下:「取得最新資料」,讓使用者知道應用程式仍在處理中。

最小化啟動時間

除了最簡單的應用程式外,所有應用程式都需要一段時間才能載入資源、剖析 XAML、設定資料結構,以及在啟用時執行邏輯。 在這裡,我們會藉由將其分成三個階段來分析啟用程序。 我們也提供減少每個階段所花費時間的秘訣,以及讓應用程式啟動的每個階段對使用者更方便使用的技巧。

啟用期間是使用者啟動應用程式到應用程式開始運作之間的時間。 這是一個關鍵的時間,因為它是使用者對應用程式的第一印象。 使用者預期系統與應用程式會有立即且持續的意見反應。 當應用程式無法快速啟動時,系統與應用程式會被視為中斷或設計不佳。 更糟的是,如果應用程式需要很長的時間才能啟動,程序生命週期管理員 (PLM) 可能會終止它,或使用者可能會將其解除安裝。

啟動階段簡介

啟動牽涉到一些移動片段,且所有片段都必須正確協調,以獲得最佳使用者體驗。 在使用者按一下應用程式磚直到顯示應用程式內容,會發生下列步驟。

  • Windows 殼層會啟動程序,並呼叫 Main。
  • 建立 Application 物件。
    • (專案範本) 建構函式會呼叫 InitializeComponent,這會導致剖析 App.xaml 並建立物件。
  • 引發 Application.OnLaunched 事件。
    • (ProjectTemplate) 應用程式程式碼會建立 Frame 並瀏覽至 MainPage。
    • (ProjectTemplate) Mainpage 建構函式會呼叫 InitializeComponent,這會導致剖析 MainPage.xaml 並建立物件。
    • 呼叫 ProjectTemplate) Window.Current.Activate()。
  • XAML 平台執行版面配置傳遞,包括測量與排列。
    • ApplyTemplate 會導致為每個控制項建立控制項範本內容,其通常是啟動的大部分版面配置時間。
  • 呼叫 Render 來建立所有視窗內容的視覺效果。
  • 畫面會顯示給桌面視窗管理員 (DWM)。

在啟動路徑中執行較少工作

從您的啟動程式碼路徑中除去第一個畫面不需要的任何其他項目。

  • 如果您有使用者 dll 包含第一個畫面期間不需要的控制項,請考慮延遲載入它們。
  • 如果您有一部分 UI 相依於雲端的資料,請分割該 UI。 首先,顯示不相依於雲端資料的 UI,並以非同步方式顯示雲端相依的 UI。 您也應該考慮在本機快取資料,讓應用程式離線運作,或不受網路連線緩慢的影響。
  • 如果您的 UI 正在等候資料,則顯示進度 UI。
  • 請謹慎處理涉及許多組態檔案剖析的應用程式設計,或由程式碼動態產生的 UI。

減少元素計數

XAML 應用程式中的啟動效能會與您在啟動期間建立的元素數目直接相關。 您建立的元素越少,應用程式啟動所需的時間就越少。 作為粗略的基準測試,請考慮每個元素需要 1 毫秒才能建立。

  • 項目控制項中使用的範本可能會有最大的影響,因為它們會重複多次。 請參閱 ListView 與 GridView UI 最佳化
  • UserControls 和控制項範本將會展開,因此也應該將這些範本納入考慮。
  • 如果您建立任何未出現在畫面上的 XAML,則應該確認是否應在啟動期間建立這些 XAML 片段。

[Visual Studio 即時視覺化樹狀結構] 視窗會顯示樹狀結構中每個節點的子元素計數。

Live visual tree.

使用延遲。 折疊元素,或將其不透明度設定為 0,不會阻止建立元素。 使用 x:Load 或 x:DeferLoadStrategy,您便可延遲載入某個 UI,而在需要時才加以載入。 這是延遲處理在啟動畫面期間看不到之 UI 的好方法,因此您可以視需要載入 UI,或做為一組延遲邏輯的一部分。 若要觸發載入,您只需要呼叫元素的 FindName。 如需範例與詳細資訊,請參閱 x:Load 屬性x:DeferLoadStrategy 屬性 \(部分機器翻譯\)。

虛擬化。 如果您的 UI 中有清單或重複項內容,強烈建議您使用 UI 虛擬化。 如果未虛擬化清單 UI,則您需預先支付建立所有元素的費用,這樣可能會減緩您的啟動速度。 請參閱 ListView 與 GridView UI 最佳化

應用程式效能不僅與原始效能有關,也與感知有關。 變更作業順序,讓視覺層面先發生,通常會讓使用者覺得應用程式更快速。 當內容出現在畫面上時,使用者才會考慮載入的應用程式。 通常,應用程式在啟動時需要執行多個動作,但顯示 UI 並不一定需要所有這些動作,因此應該延遲或排定比 UI 低的優先順序。

本主題討論來自動畫/電視的「第一個畫面」,並衡量使用者看到內容的時間長度。

改善啟動感知

我們用一個簡單的線上遊戲範例來識別啟動的每個階段和不同的技術,以在整個程序中提供使用者意見反應。 在此範例中,啟用的第一個階段是使用者點選遊戲磚到遊戲開始執行其程式碼之間的時間。 在此期間,系統沒有任何內容可向使用者顯示,甚至會顯示已啟動正確的遊戲。 但提供啟動顯示畫面會將該內容提供給系統。 接著,遊戲會透過在開始執行程式碼時,以自己的 UI 取代靜態啟動顯示畫面,藉此通知使用者啟動的第一個階段已完成。

啟用的第二個階段包括建立和初始化對遊戲至關重要的結構。 如果應用程式可以在啟用第一個階段之後使用可用的資料快速建立其初始 UI,則第二個階段是微不足道的,您可以立即顯示 UI。 否則,我們建議應用程式在初始化時顯示載入頁面。

載入頁面的外觀由您決定,最簡單的是只顯示進度列或進度環。 關鍵是讓應用程式顯示它正在執行工作,然後再變成有回應。 如果是遊戲,其想要顯示初始畫面,但 UI 需要從磁碟將一些影像和音效載入記憶體。 這些工作需要幾秒鐘的時間,因此應用程式會以載入頁面取代啟動顯示畫面以持續通知使用者,顯示與遊戲主題相關的簡單動畫。

第三個階段在遊戲取得建立互動式 UI 所需的最少資訊集之後開始,其將取代載入頁面。 此時,線上遊戲唯一可用的資訊就是應用程式從磁碟載入的內容。 遊戲會隨附足夠的內容來建立互動式 UI;但由於它是一個線上遊戲,直到連接到網際網路並下載一些額外的資訊之前是無法運作的。 直到它取得運作所需的所有資訊後,使用者才能與 UI 互動,但需要從 Web 取得其他資料的功能應該提供仍在載入內容的意見反應。 應用程式可能需要一些時間才能完全運作,因此請務必儘快提供功能。

現在我們已識別出線上遊戲啟用的三個階段,讓我們將它們繫結至實際程式碼。

階段 1

在應用程式啟動之前,它必須告訴系統想要顯示為啟動顯示畫面的內容。 其方式是將影像和背景色彩提供給應用程式資訊清單中的 SplashScreen 元素,如範例所示。 Windows 會在應用程式開始啟動之後顯示。

<Package ...>
  ...
  <Applications>
    <Application ...>
      <VisualElements ...>
        ...
        <SplashScreen Image="Images\splashscreen.png" BackgroundColor="#000000" />
        ...
      </VisualElements>
    </Application>
  </Applications>
</Package>

如需詳細資訊,請參閱新增啟動顯示畫面

僅使用應用程式的建構函式來初始化對應用程式至關重要的資料結構。 建構函式只會在第一次執行應用程式時呼叫,而且不一定每次啟動應用程式時都呼叫。 例如,不會針對已執行、置於背景,再透過搜尋協定啟動的應用程式呼叫建構函式。

階段 2

啟動應用程式的原因有很多,您可能想要以不同的方式處理。 您可以覆寫 OnActivatedOnCachedFileUpdaterActivatedOnFileActivatedOnFileOpenPickerActivatedOnFileSavePickerActivatedOnLaunchedOnSearchActivatedOnShareTargetActivated 方法來處理每個啟動原因。 應用程式在這些方法中必須執行的其中一件事是建立 UI、將它指派給 Window.Content,然後呼叫 Window.Activate。 此時,啟動顯示畫面會由應用程式建立的 UI 取代。 如果啟動時有足夠的資訊可供建立,此視覺效果可能是載入畫面或應用程式的實際 UI。

public partial class App : Application
{
    // A handler for regular activation.
    async protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        base.OnLaunched(args);

        // Asynchronously restore state based on generic launch.

        // Create the ExtendedSplash screen which serves as a loading page while the
        // reader downloads the section information.
        ExtendedSplash eSplash = new ExtendedSplash();

        // Set the content of the window to the extended splash screen.
        Window.Current.Content = eSplash;

        // Notify the Window that the process of activation is completed
        Window.Current.Activate();
    }

    // a different handler for activation via the search contract
    async protected override void OnSearchActivated(SearchActivatedEventArgs args)
    {
        base.OnSearchActivated(args);

        // Do an asynchronous restore based on Search activation

        // the rest of the code is the same as the OnLaunched method
    }
}

partial class ExtendedSplash : Page
{
    // This is the UIELement that's the game's home page.
    private GameHomePage homePage;

    public ExtendedSplash()
    {
        InitializeComponent();
        homePage = new GameHomePage();
    }

    // Shown for demonstration purposes only.
    // This is typically autogenerated by Visual Studio.
    private void InitializeComponent()
    {
    }
}
    Partial Public Class App
    Inherits Application

    ' A handler for regular activation.
    Protected Overrides Async Sub OnLaunched(ByVal args As LaunchActivatedEventArgs)
        MyBase.OnLaunched(args)

        ' Asynchronously restore state based on generic launch.

        ' Create the ExtendedSplash screen which serves as a loading page while the
        ' reader downloads the section information.
        Dim eSplash As New ExtendedSplash()

        ' Set the content of the window to the extended splash screen.
        Window.Current.Content = eSplash

        ' Notify the Window that the process of activation is completed
        Window.Current.Activate()
    End Sub

    ' a different handler for activation via the search contract
    Protected Overrides Async Sub OnSearchActivated(ByVal args As SearchActivatedEventArgs)
        MyBase.OnSearchActivated(args)

        ' Do an asynchronous restore based on Search activation

        ' the rest of the code is the same as the OnLaunched method
    End Sub
End Class

Partial Friend Class ExtendedSplash
    Inherits Page

    Public Sub New()
        InitializeComponent()

        ' Downloading the data necessary for
        ' initial UI on a background thread.
        Task.Run(Sub() DownloadData())
    End Sub

    Private Sub DownloadData()
        ' Download data to populate the initial UI.

        ' Create the first page.
        Dim firstPage As New MainPage()

        ' Add the data just downloaded to the first page

        ' Replace the loading page, which is currently
        ' set as the window's content, with the initial UI for the app
        Window.Current.Content = firstPage
    End Sub

    ' Shown for demonstration purposes only.
    ' This is typically autogenerated by Visual Studio.
    Private Sub InitializeComponent()
    End Sub
End Class

在啟動處理常式中顯示載入頁面的應用程式會開始在背景中建立 UI。 建立該元素之後,就會發生其 FrameworkElement.Loaded 事件。 在事件處理常式中,您會以新建立的首頁取代目前載入畫面的視窗內容。

具有延長初始化期間的應用程式顯示載入頁面是很重要的。 除了提供使用者關於啟動程序的意見反應之外,如果在啟動程序開始的 15 秒內未呼叫 Window.Activate,程序將終止。

partial class GameHomePage : Page
{
    public GameHomePage()
    {
        InitializeComponent();

        // add a handler to be called when the home page has been loaded
        this.Loaded += ReaderHomePageLoaded;

        // load the minimal amount of image and sound data from disk necessary to create the home page.
    }

    void ReaderHomePageLoaded(object sender, RoutedEventArgs e)
    {
        // set the content of the window to the home page now that it's ready to be displayed.
        Window.Current.Content = this;
    }

    // Shown for demonstration purposes only.
    // This is typically autogenerated by Visual Studio.
    private void InitializeComponent()
    {
    }
}
    Partial Friend Class GameHomePage
    Inherits Page

    Public Sub New()
        InitializeComponent()

        ' add a handler to be called when the home page has been loaded
        AddHandler Me.Loaded, AddressOf ReaderHomePageLoaded

        ' load the minimal amount of image and sound data from disk necessary to create the home page.
    End Sub

    Private Sub ReaderHomePageLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
        ' set the content of the window to the home page now that it's ready to be displayed.
        Window.Current.Content = Me
    End Sub

    ' Shown for demonstration purposes only.
    ' This is typically autogenerated by Visual Studio.
    Private Sub InitializeComponent()
    End Sub
End Class

如需使用延長啟動顯示畫面的範例,請參閱啟動顯示畫面範例

階段 3

應用程式顯示 UI,並不表示應用程式已完全可供使用。 在我們的遊戲案例中,顯示 UI 時會有預留位置,以供需要從網際網路取得資料的功能使用。 此時,遊戲會下載讓應用程式完整運作所需的額外資料,並隨著取得資料而逐漸啟用功能。

有時候,啟動所需的大部分內容都可以與應用程式封裝在一起。 這屬於簡單款遊戲的情況。 其啟動程序相當簡單。 但許多程式 (如新聞讀取器和相片檢視程式) 必須從網路提取資訊才能發揮作用。 這些資料可能很大,需要相當多的時間才能下載。 在啟動程序期間,應用程式如何取得此資料,可能會對應用程式的感知效能造成巨大影響。

如果應用程式嘗試在啟動的第一個或前兩個階段中下載功能所需的整個資料集,您可以顯示載入頁面,或更糟的是顯示啟動顯示畫面數分鐘。 這讓應用程式看起來就像已停止回應,或讓系統終止應用程式。 我們建議應用程式下載最少的資料量,以在階段 2 中顯示互動式 UI 和預留位置元素,然後漸進式載入資料,以在階段 3 中取代預留位置元素。 如需處理資料的詳細資訊,請參閱最佳化 ListView 和 GridView

應用程式對每個啟動階段的反應完全由您決定,但提供使用者盡可能多的意見反應 (啟動顯示畫面、載入畫面、載入資料時的 UI),讓使用者感覺不光是應用程式,還有整個系統速度都很快。

將啟動路徑中的受控組件最小化

可重複使用的程式碼通常以模組 (DLL) 形式包含在專案中提供。 載入這些模組需要存取磁碟,您可以想像,這樣做的成本可能會更高。 這對冷啟動的影響最大,但也會影響暖啟動。 在 C# 和 Visual Basic 的情況下,CLR 會視需要載入組件,嘗試盡可能延遲該成本。 也就是說,CLR 不會載入模組,直到執行的方法參考它為止。 因此,僅參考啟動程式碼中啟動應用程式所需的組件,如此 CLR 便不會載入不必要的模組。 如果您的啟動路徑中有不必要參考的未使用程式碼路徑,您可以將這些程式碼路徑移至其他方法,以避免不必要的載入。

減少模組載入的另一種方式是結合您的應用程式模組。 載入一個大型組件所需要的時間通常比載入兩個小組件少。 但這不是一律可行,只有在模組不會對開發人員生產力或程式碼重複使用性產生重大影響時,您才應該合併模組。 您可以使用 PerfViewWindows Performance Analyzer (WPA) 等工具來了解啟動時載入的模組。

提出智慧型 Web 要求

您可以將應用程式的內容封裝在本機,包括 XAML、影像,以及對應用程式很重要的任何其他檔案,藉此大幅改善應用程式的載入時間。 磁碟作業比網路作業更快。 如果應用程式在初始化時需要特定檔案,您可以從磁碟載入檔案,而不是從遠端伺服器擷取,如此可減少整個啟動時間。

有效率地記錄和快取頁面

Frame 控制項提供瀏覽功能。 其提供瀏覽至 Page (Navigate 方法)、瀏覽日誌 (BackStack/ForwardStack 屬性、GoForward/GoBack 方法)、頁面快取 (Page.NavigationCacheMode) 和序列化支援 (GetNavigationState 方法)。

Frame 需要留意的效能主要是關於日誌和頁面快取。

Frame 日誌。 當您瀏覽至具有 Frame.Navigate() 的頁面時,目前頁面的 PageStackEntry 會新增至 Frame.BackStack 集合。 PageStackEntry 相對較小,但 BackStack 集合大小沒有內建的限制。 使用者有可能在迴圈中瀏覽,並無限期地擴充此集合。

PageStackEntry 也包含傳遞至 Frame.Navigate() 方法的參數。 建議參數為基本可序列化類型 (例如 int 或 string),以允許 Frame.GetNavigationState() 方法運作。 但是,該參數可能會參考一個物件,該物件會占更多工作集或其他資源,讓 BackStack 中的每個項目成本更高。 例如,您可能會使用 StorageFile 做為參數,因此 BackStack 會使無限數量的檔案保持開啟。

因此,建議盡量保持較小的瀏覽參數,並限制 BackStack 的大小。 BackStack 是標準向量 (C# 中的 IList、C++/CX 中的 Platform::Vector),因此只要移除項目即可加以修剪。

頁面快取。 根據預設,當您使用 Frame.Navigate 方法瀏覽至頁面時,會具現化頁面的新執行個體。 同樣地,如果您接著使用 Frame.GoBack 瀏覽回上一頁,則會配置上一頁的新執行個體。

不過,Frame 提供可避免這些具現化的選擇性頁面快取。 若要取得放入快取的頁面,請使用 Page.NavigationCacheMode 屬性。 將模式設定為 [必要] 會強制快取頁面,將它設定為 [已啟用] 將允許快取。 根據預設,快取大小為 10 頁,但可以使用 Frame.CacheSize 屬性覆寫此設定。 將快取所有必要頁面,而且如果少於 CacheSize 的必要頁面,也可以快取已啟用頁面。

頁面快取可藉由避免具現化來協助提升效能,進而改善瀏覽效能。 頁面快取可能會因過度快取而損害效能,因而影響工作集。

因此,建議針對您的應用程式適當地使用頁面快取。 例如,假設您有一個應用程式會顯示 Frame 中的項目清單,且當您點選項目時,它會將框架瀏覽至該項目的詳細資料頁面。 清單頁面應該設定為快取。 如果所有項目的詳細資料頁面都相同,可能也應該快取。 但是,如果詳細資料頁面更為異質,則最好關閉快取。