ListView の項目レンダリングの最適化

コレクションを利用する JavaScript を使って作成された Windows ストア アプリの場合、アプリのすばらしいパフォーマンスを実現するには、通常、WinJS ListView コントロール (英語) とのスムーズな連携がきわめて重要です。これは意外なことではなく、数千個にもなり得る項目の管理と表示を処理する場合、項目に対するすべての最適化がものを言います。最も重要なことは、各項目がどのように表示されるか、つまり、ListView コントロール内の各項目がどのように、また、どのタイミングで、DOM によって組み立てられ、アプリの一部として表示されるかです。実際、このプロセスの "タイミング" の部分は、リスト内ですばやくパン操作を行った場合に、そのスピードに遅れずにリストの表示が変わることが期待されている場合は、特に重要な要素になります。

ListView 内でレンダリングされる項目は、HTML で定義された宣言型テンプレートか、リスト内の項目ごとに呼び出されるカスタムの JavaScript "レンダリング関数" によって処理されます。宣言テンプレートを使う方法が最も簡単ですが、このプロセスに対して特定の制御を行う場合の自由度は限られます。一方、レンダリング関数を使うと、項目ごとにレンダリングをカスタマイズでき、HTML ListView パフォーマンス最適化のサンプル (英語) に実装されているように、複数の最適化が可能です。この関数では、次のような最適化を実現できます。

  • 項目データと表示要素の非同期配信の実現。これには、基本のレンダリング関数を使用します。
  • ListView の全体的なレイアウトを決めるために必要な、項目の形状の作成と、内部の要素との分離。これには、プレースホルダー レンダラーを使用します。
  • 以前作成した項目要素 (とその子) のデータを置き換えて再利用。要素作成の大半のステップを省略できます。これには、プレースホルダー再利用レンダラーを使用します。
  • 項目が表示され、ListView のパン操作のスピードが落ち着くまで、イメージの読み込みやアニメーションなど、負荷の高いビジュアル操作の遅延。これには、多段階レンダラーを使用します。
  • 同じビジュアル操作をまとめて、DOM の再レンダリングを最小限に抑制。これには、多段階レンダラーを使用します。

この記事では、上記の全ステージを取り上げ、各ステージが ListView の項目レンダリング プロセスとどのように連携するかを説明します。お察しのとおり、項目をレンダリングするタイミングの最適化には、非同期操作、ひいては promise を多用します。したがって、この記事を読み進むにつれて、promise 自体の理解も、このブログの前回の記事「promise の詳細」(英語) の知識からさらに深めることができます。

すべてのレンダラーに言えることですが、常に、中心となる項目のレンダリング時間 (遅延操作を除く) を最小限に抑えることが重要です。ListView の最終的なパフォーマンスは、ListView の更新と画面の更新間隔がどの程度同期するかに大きく左右されるため、1 つの項目レンダラーで数ミリ秒余分にかかると、次の画面の更新までの間の ListView 全体のレンダリング時間が増大し、結果としてフレーム落ちや画面の乱れが発生します。つまり、項目レンダラーは、JavaScript コードの最適化を図る場合、本当に有効な領域です。

基本レンダラー

まず、項目レンダリング機能 (以降は、シンプルに "レンダラー" と呼びます) とはどのようなものかを簡単に確認しましょう。レンダラーは、テンプレート名の代わりに、ListView の itemTemplate (英語) プロパティに割り当てられる関数で、この関数は ListView から DOM に追加される項目に対して、必要に応じて呼び出されます (レンダラーの基本的な情報については、itemTemplate を参照してください。ただし、この最適化が本当によくわかるのはサンプルの方です)。

項目レンダリング関数は、単純に ListView のデータ ソースから項目が渡されたら、その特定の項目に必要な HTML 要素を作成し、ListView が DOM に追加できるルート要素を返すと思われるでしょうか。基本的にはそうですが、さらに 2 点、考慮することがあります。まず、項目データ自体が非同期に読み込まれる可能性があるため、要素の作成をそのデータが提供されるタイミングに合わせる方がよいでしょう。また、項目自体のレンダリング プロセスには、リモート URI からのイメージの読み込みや、項目データで指定されている他のファイルのデータの読み取りなど、他の非同期作業も含まれる場合があります。この記事で説明するさまざまなレベルの最適化では、実際、項目の要素が要求されてから、要求された項目が実際に提供されるまでの非同期の度合いを自由に決定できます。

したがって、やはり、promise が使われると予測できます。まず、ListView では項目データを直接レンダラーに渡さず、そのデータの promise を提供します。また、項目のルート要素を直接返す関数の代わりに、その要素の promise を返します。この方法により、ListView ではさまざまな項目レンダリング promise を組み合わせて、ページ全体の項目がレンダリングされるまで、(非同期に) 待機できます。実際、このように処理することで、ListView では複数のページの作成方法を管理しています。まず、表示される項目のページを作成したら、ユーザーのパン操作の結果、この次に表示されると思われる前後の 2 種類のページを非表示で作成します。また、このような promise をすべて用意しておけば、ユーザーのパン操作によって画面が変わった場合、表示が完了していない項目のレンダリングを ListView によって簡単にキャンセルでき、無駄な要素作成を行わずに済みます。

これらの promise の使用方法は、サンプルの simpleRenderer 関数を見るとわかります。

 

 function simpleRenderer(itemPromise) {
    return itemPromise.then(function (item) {
        var element = document.createElement("div");
        element.className = "itemTempl";
        element.innerHTML = "<img src='" + item.data.thumbnail +
            "' alt='Databound image' /><div class='content'>" + item.data.title + "</div>";
        return element;
    });
}

このコードでは、最初に、completed ハンドラーを itemPromise にアタッチしています。ハンドラーは、項目データが提供され、それを受けて要素が実際に作成された時点で呼び出されます。ただし、実は要素を直接返さず、その要素がフルフィルメント (提供) される promise を返していることに、改めて注意してください。つまり、itemPromise.then() からの戻り値は、ListView によって要素が必要になった場合に必要なタイミングで、"element" がフルフィルメントされる promise です。

promise を返すことで、必要に応じて他の非同期作業を実行できます。この場合は、レンダラーは単に中間の promise をチェーンにまとめ、チェーン内の最後の then から promise を返しています。以下に例を示します。

 function someRenderer(itemPromise) {
    return itemPromise.then(function (item) {
        return doSomeWorkAsync(item.data);
    }).then(function (results) {
        return doMoreWorkAsync(results1);
    }).then(function (results2) {
        var element = document.createElement("div");
        // use results2 to configure the element
        return element;
    });
}

これは、最後の then 呼び出しから promise を返しているので、チェーンの最後に done を使わないケースになります。エラーが発生した場合は、ListView によってエラーは処理されます。

プレースホルダー レンダラー

ListView の最適化の次の段階では、要素の作成を 2 段階に分ける "プレースホルダー レンダラー" を使います。このレンダラーを使うことで、ListView から、リスト全体のレイアウトの定義に必要な要素の部分のみを要求でき、各項目内の要素すべてを作成する必要がありません。その結果、ListView はレイアウト パスを短時間で完成して、この後の入力でも高い応答性を保つことができます。パスが完成したら、後で、要素の残りの部分を要求できます。

プレースホルダー レンダラーでは、promise を 1 つだけ返すのではなく、以下の 2 つのプロパティを持つオブジェクトが 1 つ返されます。

  • "element": 項目の構造内の最上位の要素で、項目のサイズと形状を定義できる必要最低限の、項目データに依存しない要素を示します。
  • "renderComplete": 要素のコンテンツの残りが作成された時点で、フルフィルメントされる promise を示します。つまり、前と同様に itemPromise.then で始まるチェーンから promise を返します。

ListView では、レンダラーが promise を返す (前と同様に基本のケース) か、element および renderComplete プロパティを返す (より高度なケース) かどうかを確認できます。したがって、前回の simpleRenderer と同等のプレースホルダー レンダラー (サンプル内) は次のようになります。

 function placeholderRenderer(itemPromise) {
    // create a basic template for the item that doesn't depend on the data
    var element = document.createElement("div");
    element.className = "itemTempl";
    element.innerHTML = "<div class='content'>...</div>";

    // return the element as the placeholder, and a callback to update it when data is available
    return {
        element: element,

        // specifies a promise that will be completed when rendering is complete
        // itemPromise will complete when the data is available
        renderComplete: itemPromise.then(function (item) {
            // mutate the element to include the data
            element.querySelector(".content").innerText = item.data.title;
            element.insertAdjacentHTML("afterBegin", "<img src='" +
                item.data.thumbnail + "' alt='Databound image' />");
        })
    };
}

element.innerHTML の割り当ては、renderComplete 内に移動することもできます。これは、サンプルの css/scenario1.css ファイルの itemTempl クラスで、項目の幅と高さを直接指定しているためです。element プロパティに含まれている理由は、プレースホルダーに既定の "…" テキストを提供するためです。方法は簡単で、すべての項目で共有される、小さいパッケージ内リソースを参照する (したがって短時間でレンダリングされる) img 要素を使用するだけです。

プレースホルダーの再利用レンダラー

次の最適化の "プレースホルダーの再利用" レンダラーでは、promise に関しては、新しいものは何もありません。recycled というレンダラーにある 2 つ目のパラメーターに注目してください。これは、前にレンダリングされていて、現在は非表示になっている項目のルート要素です。つまり、再利用された要素には既に子要素が作成されているので、データを置き換えて、おそらく子要素のいくつかを調整するだけです。これにより、再利用をせず、まったく新しい項目をレンダリングする際に必要になる、不可の高い要素作成呼び出しのほとんどを回避し、レンダリング プロセスの時間を大幅に節約できます。

ListView は、loadingBehavior (英語) を "randomaccess" に設定すると、再利用した要素を提供できます。"recycled" が指定された場合、要素 (とその子要素) からデータを消去し、プレースホルダーとしてこれを返します。その後、データを設定し、(必要に応じて) renderComplete 内に追加の子を作成します。再利用された要素が提供されない場合 (ListView が初めて作成されたか、loadingBehavior が "incremental" の場合)、要素を新たに作成します。以下は、その動作のコードをサンプルから抜粋したものです。

 function recyclingPlaceholderRenderer(itemPromise, recycled) {
    var element, img, label;
    if (!recycled) {
        // create a basic template for the item that doesn't depend on the data
        element = document.createElement("div");
        element.className = "itemTempl";
        element.innerHTML = "<img alt='Databound image' style='visibility:hidden;'/>" +
            "<div class='content'>...</div>";
    }
    else {
        // clean up the recycled element so that we can reuse it 
        element = recycled;
        label = element.querySelector(".content");
        label.innerHTML = "...";
        img = element.querySelector("img");
        img.style.visibility = "hidden";
    }
    return {
        element: element,
        renderComplete: itemPromise.then(function (item) {
            // mutate the element to include the data
            if (!label) {
                label = element.querySelector(".content");
                img = element.querySelector("img");
            }
            label.innerText = item.data.title;
            img.src = item.data.thumbnail;
            img.style.visibility = "visible";
        })
    };
}

"renderComplete" では、"label" など、新しいプレースホルダーに作成しない要素があるかを必ず確認し、必要に応じてここで作成してください。

より一般的に再利用された項目からデータを消去する場合は、ListView の resetItem (英語) プロパティに関数を設定できます。この関数には、上記と同様のコードが含まれます。項目だけでなくグループ ヘッダーにもテンプレート関数を使用できるので、resetGroupHeader (プロパティ) についても同じことが言えます。グループ ヘッダーははるかに少なく、通常、予想されるパフォーマンスは同じになるので、これまでそれほど詳しくは説明していませんが、この機能は存在しています。

多段階レンダラー

次は、最後から 2 番目の最適化、"多段階レンダラー" です。このレンダラーは、プレースホルダー再利用レンダラーを拡張して、DOM に項目の残りの要素が完全に追加されるまで、イメージや他のメディアの読み込みを遅延します。また、項目が実際に画面に表示されるまで、アニメーションなどの効果も遅延します。これは、ユーザーが ListView 内ではかなりすばやくパンを行うことが多いので、ListView の動きが落ち着くまで、負荷の高い操作を非同期に遅延することが有効なためです。

ListView は、itemPromise から生成された item のメンバーとして必要なフックを提供します。これらは、ready というプロパティ (promise) と、(さらに) promise を返す loadImage および isOnScreen の 2 つのメソッドです。以下が、そのコードです。

 renderComplete: itemPromise.then(function (item) {
    // item.ready, item.loadImage, and item.isOnScreen available
})

これらの使い方を説明します。

  • ready チェーン内で最初に completed ハンドラーからこの promise を返します。この promise は、要素の構造全体がレンダリングされ、表示できるようになった時点で、フルフィルメントされます。つまり、completed ハンドラーを使って別の then をチェーンし、そのチェーン内で、イメージの読み込みなど、他の表示後の作業を実行できます。
  • loadImage URI からイメージをダウンロードして、指定されている "img" 要素内に URI 表示し、この要素によってフルフィルメントされる promise を返します。この promise に completed ハンドラーをアタッチします。このハンドラー自体は、isOnScreen から promise を返します。"img" 要素が提供されていない場合は、loadImage によって作成され、completed ハンドラーに渡されます。
  • isOnScreen 項目が表示されるかどうかを示すブール値をフルフィルメント値として取る promise を返します。現在の実装では、これは既知の値になるため、promise は同期的にフルフィルメントされます。ただし、これを promise を使ってラップすることで、より長いチェーンで使用できます。

これらはすべてサンプルの multistageRenderer 関数内で使われています。この関数では、イメージの読み込みの完了を合図に、フェード イン アニメーションを開始します。以下に、"renderComplete" promise から返される内容のみを示します。

 renderComplete: itemPromise.then(function (item) {
    // mutate the element to update only the title
    if (!label) { label = element.querySelector(".content"); }
    label.innerText = item.data.title;

    // use the item.ready promise to delay the more expensive work
    return item.ready;
    // use the ability to chain promises, to enable work to be cancelled
}).then(function (item) {
    // use the image loader to queue the loading of the image
    if (!img) { img = element.querySelector("img"); }
    return item.loadImage(item.data.thumbnail, img).then(function () {
        // once loaded check if the item is visible
        return item.isOnScreen();
    });
}).then(function (onscreen) {
    if (!onscreen) {
        // if the item is not visible, don't animate its opacity
        img.style.opacity = 1;
    } else {
        // if the item is visible, animate the opacity of the image
        WinJS.UI.Animation.fadeIn(img);
    }
})

ここではさまざまな処理をしていますが、基本の promise チェーンしかないことに変わりありません。レンダラー内の最初の同期操作によって、テキストなど、項目の要素構造でシンプルな要素のみが更新されます。次に、item.ready で promise を返しています。promise がフルフィルメントされるとき (より正確には、その promise が実行される場合) に、項目の非同期の loadImage メソッドを使ってイメージのダウンロードを開始し、item.isOnScreen promise をその completed ハンドラーから返します。つまり、可視性を示す "onscreen" フラグがチェーン内で最後に completed ハンドラーに渡されます。その isOnScreen promise がフルフィルメントされる場合、つまり、項目が実際に表示される時点で、アニメーションなどの関連操作を実行できます。

"場合" を強調するのは、これらの処理が行われている最中に、ユーザーが ListView 内でパン操作を行う可能性があるためです。この場合も、これらの promise すべてをチェーンにまとめることで、これらの項目がパン操作によってビューやバッファー済みのページから外れた場合に、ListView では非同期操作をキャンセルできます。ListView コントロールに対して、ここまでで "さまざまな" パフォーマンス テストが実行されたと言えるでしょう。

また、やはり "renderComplete" プロパティ内のレンダリング関数から promise を返しているため、これらのどのチェーンでも全体で then を使用していることに改めて注意してください。これらのレンダラー内でチェーンの最後に到達することはないため、done をレンダラーの最後に使うことはありません。

縮小表示のバッチ処理

最後の最適化は、まさに ListView コントロールにとって "最後の一撃" です。以下のコードの batchRenderer という関数内に、"renderComplete" を対象とするこの構造があります (ほとんどのコードは省略)。

 renderComplete: itemPromise.then(function (item) {
    // mutate the element to update only the title
    if (!label) { label = element.querySelector(".content"); }
    label.innerText = item.data.title;

    // use the item.ready promise to delay the more expensive work
    return item.ready;
    // use the ability to chain promises, to enable work to be cancelled
}).then(function (item) {
    // use the image loader to queue the loading of the image
    if (!img) { img = element.querySelector("img"); }
    return item.loadImage(item.data.thumbnail, img).then(function () {
        // once loaded check if the item is visible
        return item.isOnScreen();
    });
}).then(function (onscreen) {
    if (!onscreen) {
        // if the item is not visible, don't animate its opacity
        img.style.opacity = 1;
    } else {
        // if the item is visible, animate the opacity of the image
        WinJS.UI.Animation.fadeIn(img);
    }
})

これは、multistageRenderer とほぼ同じですが、item.loadImage 呼び出しと item.isOnScreen チェックの間にある、この不思議な thumbnailBatch という関数への呼び出しが挿入されている点が異なります。チェーン内に thumbnailBatch がある場合、これは、その戻り値が、別の promise を返す completed ハンドラーになる必要があることを示しています。

わかりにくいでしょうか? これについては、詳しく説明しますが、最初に、ここで何をしようとしているか、少し背景を説明する必要があります。

項目が 1 つしかない ListView だけなら、さまざまな読み込みの最適化の効果はわからないでしょう。しかし、通常、ListViews には多数の項目があり、レンダリング関数は項目ごとに呼び出されます。前のセクションの multistageRenderer 内で、各項目のレンダリングによって、非同期の item.loadImage 操作が開始され、任意の URI から縮小表示がダウンロードされます。この操作のそれぞれに、時間がかかります。したがって、リスト全体では、多数の loadImage 呼び出しが同時に実行され、特定の縮小表示が完成するのを各項目のレンダリングが待機することになる可能性があります。ここまではよいでしょう。

ただし、multistageRenderer からは計り知れない重要な特徴は、縮小表示の "img" 要素は "既に" DOM 内にあり、ダウンロードの完了しだい、loadImage 関数によってそのイメージの src 属性が設定されることです。属性が設定されたら、promise チェーンの残りが返されしだい、レンダリング エンジンが更新されます。これ以降は、基本的に同期的に処理されます。

さらにその後、多数の縮小表示が、短時間のうちに UI スレッドに返される可能性があります。これにより、レンダリング エンジンに過剰なチャーンが発生し、ビジュアル パフォーマンスがかなり低下します。このチャーンを避けるため、これらの "img" 要素が DOM に渡される "前" に、完全に img 要素を作成して、バッチに追加し、これらの要素がすべて単一のレンダリング パスで処理されるようにします。

サンプルでは、promise の一部 (createBatch という魔法の関数) で、これを実装しています。createBatch は、アプリ全体で 1 度だけ呼び出され、その結果 (別の関数) は thumbnailBatch という名前の変数に格納されます。

 var thumbnailBatch;
thumbnailBatch = createBatch();

この thumbnailBatch 関数への呼び出しは、これ以降参照することになりますが、やはりレンダラーの promise チェーンに挿入されます。この挿入の目的は、この後すぐに説明するバッチ処理コードの性質を踏まえて、読み込まれた複数の "img" 要素をグループにまとめ、適切な間を置いて次の処理に渡すことです。レンダラーの promise チェーンだけを見ても、やはり thumbnailBatch() の呼び出しは、promise を返す completed ハンドラー関数を返す必要があります。また、その promise のフルフィルメント値 (チェーンの次のステップを参照) は、DOM に追加できる "img" 要素である必要があります。バッチ処理が実行された "後" で、これらのイメージを DOM に追加することで、グループ全体を同じレンダリング パスにまとめています。

これは、batchRenderer と、前のセクションの multistageRenderer との重要な違いです。後者では、縮小表示の "img" 要素が既に DOM 内に存在し、2 番目のパラメーターとして loadImage に渡されます。したがって、loadImage がイメージの "src" 属性が設定されるときに、レンダリングの更新が発生します。ただし、batchRenderer 内では、その "img" 要素は、loadImage 内 (ここでも "src" が設定されている) で別に作成されますが、"img" はまだ DOM に追加されていません。thumbnailBatch ステップが完了しないと DOM に追加されません。これにより、同一のレイアウト パス内にグループを組み込んでいます。

では、このバッチ処理のしくみを見ていきましょう。以下は、createBatch 関数の全容です。

 function createBatch(waitPeriod) {
    var batchTimeout = WinJS.Promise.as();
    var batchedItems = [];

    function completeBatch() {
        var callbacks = batchedItems;
        batchedItems = [];
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i]();
        }
    }

    return function () {
        batchTimeout.cancel();
        batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);

        var delayedPromise = new WinJS.Promise(function (c) {
            batchedItems.push(c);
        });

        return function (v) {
            return delayedPromise.then(function () {
                return v;
            });
        };
    };
}

この場合も、createBatch は "1 度だけ" 呼び出され、その結果の thumbnailBatch がリスト内のレンダリングされた項目のそれぞれに対して呼び出されています。thumbnailBatch が生成した completed ハンドラーは、loadImage 操作が完了するたびに呼び出されます。

このような completed ハンドラーは、同程度の手間で、直接レンダリング関数に挿入することもできますが、ここでは、項目ごとではなく、複数の項目全体でアクティビティを調整することが目的です。この調整は、createBatch の始めに作成および初期化される、2 つの変数によって実現されます。空の promise を初期化している "batchedTimeout" と、最初は空の関数の配列を初期化している "batchedItems" です。createBatch では関数 completeBatch も宣言していて、配列内の各関数を呼び出し、batchedItems を空にします。

 function createBatch(waitPeriod) {
    var batchTimeout = WinJS.Promise.as();
    var batchedItems = [];

    function completeBatch() {
        var callbacks = batchedItems;
        batchedItems = [];
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i]();
        }
    }

    return function () {
        batchTimeout.cancel();
        batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);

        var delayedPromise = new WinJS.Promise(function (c) {
            batchedItems.push(c);
        });

        return function (v) {
            return delayedPromise.then(function () {
                return v;
            });
        };
    };
}

今度は、thumbnailBatch (createBatch から返される関数) 内の処理を見ていきましょう。これも、レンダリングされる項目ごとに呼び出されます。最初に、既存の batchedTimeout をキャンセルし、すぐに作成し直します。

 batchTimeout.cancel();
        batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);

2 行目は、「promise の詳細」<TODO: link> の記事で説明している、将来の配信/フルフィルメント パターンです。ここでは、"waitPeriod" に指定された遅延 (ミリ秒単位、既定値は 64 ミリ秒) の後に、completeBatch を呼び出しています。したがって、thumbnailBatch が以前の呼び出しの "waitPeriod" で再び呼び出される限り、batchTimeout は新しい waitPeriod にリセットされます。また、thumbnailBatch は、item.loadImage 呼び出しが完了した "後で" しか呼び出されないため、以前の呼び出しの "waitPeriod" 内で完了する loadImage 操作は同じバッチ内に含めることができます。"waitPeriod" に指定されている間隔を過ぎると、バッチが処理され (DOM にイメージが追加され)、次のバッチが開始されます。

このタイムアウト操作が処理されたら、thumbnailBatch によって新しい promise が作成され、この promise が完了ディスパッチャー関数を "batchedItems" 配列にプッシュします。

 var delayedPromise = new WinJS.Promise(function (c) {
         batchedItems.push(c);
     });

「promise の詳細」<TODO: link> では、promise はコード コンストラクトに過ぎず、ここにはそれしかありません。新しく作成される promise 自体には、非同期動作はありません。完了ディスパッチャー関数の "c" を "batchedItems" に追加しているだけです。ただし、もちろん、"batchedTimeout" が非同期に完了するまで、ディスパッチャーによる処理は何も実行されないため、ここでは実際に非同期な関係が成立しています。タイムアウトが発生し、バッチを消去したら (completeBatch 内)、別の場所にある completed ハンドラーを delayedPromise.then に呼び出します。

今度は、createBatch の最後のコード行です。これは、thumbnailBatch が実際に返される関数です。この関数こそが、レンダラーの promise チェーン全体に挿入される completed ハンドラーです。

 return function (v) {
           return delayedPromise.then(function () {
               return v;
           });
       };

実際、このコードを直接 promise チェーンに挿入し、最終的な関係を見てみましょう。

 return item.loadImage(item.data.thumbnail);
          }).then(function (v) {
              return delayedPromise.then(function () {
                  return v;
              });
          ).then(function (newimg) {

これで、引数 "v" が item.loadImage の結果であることがわかります。これは作成された "img" 要素です。バッチ処理を行わない場合は、return WinJS.Promise.as(v) と記述するだけでも、チェーン全体は機能します。その場合 "v" は同期的に渡され、次のステップの "newimg" に設定されます。

しかし、ここでは、delayedPromise.then から promise を返しています。これは、現在の "batchedTimeout" がフルフィルメントされるまで、("v" によって) 実行されません。その時点で (ここでも 1 つ目の loadImage が完了してから "waitPeriod" で指定されている間隔が空けられます)、"img" 要素がチェーン内の次のステップに渡され、そこで DOM に追加されます。

以上です。

終わりに

HTML ListView パフォーマンス最適化のサンプル (英語) に実装されている 5 種類のレンダリング関数すべてに共通することが、1 つあります。それは、promise によって表現される、ListView とレンダラー間の非同期の関係によって、レンダラーがリスト内の項目の要素を生成する方法とタイミングを非常に柔軟にコントロールできることです。独自のアプリを作成する際に、ListView の最適化にどのような戦略を使うかは、データ ソースのサイズ、項目自体の複雑さ、(リモート イメージのダウンロードなど) それらの項目に非同期で取得するデータの量に大きく依存します。それでも、パフォーマンス目標を達成できるように、項目レンダラーはできるだけシンプルにしたいと思うのは当然です。しかし、いずれにせよ、ListView (とアプリ) のパフォーマンスを最大限引き出すために必要なツールは、これですべて揃いました。

Kraig Brockschmidt

- Windows エコシステム担当チーム、プログラム マネージャー

著書『HTML、CSS、JavaScript を使った Windows 8 アプリ開発』(英語)