スタッフブログ Staff Blog

表示パフォーマンスの改善事例紹介(JavaScript編)

オフタイムに趣味で閲覧しているとあるサイトにおいて、表示完了までに約8秒(Firefoxを使用)の時間を要するサイトがありました。仕事ではないのであまり気にかけてはいなかったのですが、他の閲覧者の方から「IEで見ると40秒以上かかるのですが...」という話が上がり、フロントエンド・エンジニアとして少し調べてみるとにしました。

時間がかかっているのはJavaScript

実際にIEで閲覧してみると確かに非常に長い時間待たされます。ただ画面には「読み込み中」のテキストが表示されているので、「もしかするとブラウザでの処理に時間がかかっているのか」と考えました。

そこでIEの開発者向けツールでプロファイリングをしてみると、長い時間待っているはスクリプトの処理が続いていることが分かりました。(赤い棒が長時間にわたり伸びています。)

プロファイリング結果からコードの問題の特定

ブラウザを仕事の際に普段使用しているChromeに戻し、再度プロファイリングを取ってみました。結果、下記のようなデータを得ることができました。
Chromeの開発者ツールで取得したプロファイリング結果の表示

画像では長時間処理に時間がかかっている部分にフォーカスしています。XMLHTTPRequestの処理に時間がかかっているのですが、さらに探っていくとsuccess内の処理に問題がありそうなことが分かります。JavaScriptのファイル名と行数も表示されているので、示された部分(下記サンプルコード)を中心に見ていくことにします。

$.ajax({
  url: "/xml/entries.xml",
  dataType: "xml",
  success: function(xml) {
    var layer = $('<div id="news" />');
    var ul = $('<ul />');
    $(xml).find("item").each(function(i) {
      var tag = $('<li class="item"><span class="date">' + $(this).find("entryDate").text() + '</span><spa class="title">' + $(this).find("entryTitle").text() + '</span></li>');
      layer.append(ul.append(tag));
    });
    layer.append(ul);
  }
});

先の画像には表示されていないのですが、開発者ツール内のBottom Upタブの先頭には「appendChild」や「Recalculate Style」が表示されていました。以上のことから問題はlayer.append(ul.append(tag));にあると推測します。もっともループ内で.append()を実行するとパフォーマンスに影響を与えることは知っていたので、ここが問題だと確信していました。

※jQueryのコードを読めば分かるのですが、.append()の中でJavaScriptネイティブの.appendChild()をコールしています。

.append()処理の改善

そこでループ内で.append()を実行することをやめ、下記サンプルコードのようにループ内ではHTMLの文字列を連結しループ外で.append()を実行するようにしたところ、パフォーマンスは大幅に改善。Chromeで約7.5秒要していた処理が約2秒に、またIEでは40秒以上要していた処理が約3秒で完了するようになりました。

$.ajax({
  url: "/xml/entries.xml",
  dataType: "xml",
  success: function(xml) {
    var layer = $('<div id="news" />');
    var ul = $('<ul />');
    var lists = '';
    $(xml).find("item").each(function(i) {
      var tag = $('<li class="item"><span class="date">' + $(this).find("entryDate").text() + '</span><spa class="title">' + $(this).find("entryTitle").text() + '</span></li>');
      lists += html;
    });
    ul.append($(lists));
    layer.append(ul);
  }
});

ただ、これでもBottom Upタブの先頭には「Recalculate Style」が出てきます。ふと開発者ツールのElementsタブでul要素を見てみると、非常に多くのli要素が追加されていることに気付きました。XMLには必要以上の記事が出力されていたようです。

そこで明らかに表示されない記事をDOMに追加しないように改善したところ、パフォーマンスはさらに改善しChromeでは一瞬で表示が完了するようになりました。まだ「Forced reflow」の警告が出ていたりコードに気になる点がありますが、本記事ではここまでの紹介とさせて頂きます。

長期間の運用に伴って落ちた表示パフォーマンス

この事例では、サイト運用開始当初は表示パフォーマンスの問題はなかったと推測します。恐らく新規構築で記事数が少ないために、表示パフォーマンス上良くないコードであったとしても処理がすぐに終わっていたのだろうと考えられます。

最初に負荷テストを実行して問題がないか確かめることもできますしコーディングのベストプラクティスを適用することで避けることも可能ですが、運用開始後定期的に表示パフォーマンスに問題はないかチェックすることも重要ではないかと考えます。

Vue.jsに触れてみて

半年程前から『Vue.js(ビュージェイエス)』を扱うようになりました。Vueは、公式サイトでは「ユーザーインターフェイスを構築するためのプログレッシブフレームワーク」と紹介されています。リアクティブなデータバインディングとコンポーネントシステム等が特徴と言われますが、その辺りは「はじめに - Vue.js」から順にお読み頂いた方が理解しやすいでしょう。

本記事では、私がVueを触って感じたメリットなどをいくつかご紹介したいと思います。

低い学習コスト

以前Angularを使用してアプリケーションを組んだことがありますが、機能が多く学習に時間がかかりそうだなという印象を持ちました。しかし、Vueは数時間手を動かしながら学習することでコアな機能は習得できるかと思います。株式会社ピクセルグリッドさまが毎週配信されている『CodeGrid』でもちょうど初心者向けの連載が始まっていたところで、私はそれを読みながら学習を進め、あっという間に一つのアプリケーションを作成することができました。

現在は公式ドキュメント(日本語版)を読み、記事にはなかった情報などを習得しています。公式ドキュメントも親しみやすく、Vueの知識を付けるには最適でしょう。

見通しの良いコードの実現

Movable Typeを使用して何かの情報...例えば病院のリストや金融機関のリスト、ニュースなどの記事をJSONやXMLで出力し、一覧表示するケースはよくあるのではないかと思います。Vueではテンプレートとロジックを容易に分離でき、見通しの良いコードに仕上げることができます。

例えば、最近私が見たとあるサイトでは次のようにしてXMLを読み込みHTMLを組み立てていました。
(元のコードは実行速度に難があったため、チューニングしたものを掲載します。)

$.ajax({
  url: "news.xml",
  dataType: "xml",
  success: function(xml) {
    var ul = document.createElement('ul');
    var lists = [];
    var items = xml.getElementsByTagName('item');

    for (var i = 0, nItems = items.length; i < nItems; i += 1) {
        var date = items[i].getElementsByTagName('entryDate')[0].textContent;
        var text = items[i].getElementsByTagName('entryTitle')[0].textContent;
        var html = '<li class="item"><a href="#" data-itemindex="' + i + '">' +
                    '<span class="date">' + date + '</span><span class="title">' +
                    text + '</a></li>';
        lists.push(html);
    }

    ul.innerHTML = lists.join('');
  }
});

Vueを使うと、以下のようにテンプレートとXMLを読み込むロジックを分離することができます。(ここでは.vue拡張子の単一ファイルコンポーネントの仕組みを利用しています。)記事データをdataオブジェクトのentriesプロパティ入れるとビューの更新が行われ、即座に記事リストが表示されます。

<template>
  <ul>
    <li v-for="entry of entries" :key="entry.id" class="item">
      <a href="#"><span class="date">{{entry.date}}</span><span class="title">{{entry.title}}</span></a>
    </li>
  </ul>
</template>

<script>
export default {
  data () {
    return {
      entries: []
    }
  },
  created () {
    $.ajax({
      url: 'news.xml',
      dataType: 'xml'
    }).done((xml) => {
      const items = xml.getElementsByTagName('item');
      let entries = [];

      for (let i = 0, nItems = items.length; i < nItems; i += 1) {
        const id = items[i].getElementsByTagName('entryID')[0].textContent;
        const title = items[i].getElementsByTagName('entryTitle')[0].textContent;
        const date = items[i].getElementsByTagName('entryDate')[0].textContent;
        entries.push({
          index: i,
          id: id,
          title: title,
          date: date
        });
      }

      this.entries = entries;
    });
  },
};
</script>

アプリケーションにはリスト表示以外のUIやメソッドも必要でしょうから、どちらがコードを記述しやすいか、またどちらがメンテナンスしやすいか、は自ずと分かるのではないかと思います。

リアクティブシステムでロジックの複雑さを回避

記事一覧にページネーションを実装する例について考えてみましょう。

まず、dataオブジェクトへ現在のページ・1ページの表示記事数・最大ページを追加します。そして「次のページへ」「前のページへ」がクリックされた時は、dataオブジェクトの現在のページ数のみ変更します。現在のページ数が変更されると自動的にdispEntriesメソッドが呼ばれ、指定したページを表示するために必要な記事を抜き出してビューに反映できるようになりました。

ボタンをクリックした時のメソッド内でビューを更新するメソッドを呼ぶ必要がないため、メソッドの呼び出しがあちこちに点在し蜘蛛の巣のように絡み合うような事態を避けることができるのが大きなメリットではないかと感じました。

<template>
  <div id="news">
    <ul>
      <li v-for="entry of dispEntries" :key="entry.id" class="item">
        <a href="#"><span class="date">{{entry.date}}</span><span class="title">{{entry.title}}</span></a>
      </li>
    </ul>
    <button @click="loadPrevPage" v-if="page !== 0">前のページ</button>
    <button @click="loadNextPage" v-if="page !== maxPages">次のページ</button>
  </div><!-- /#news -->
</template>

<script>
export default {
  data () {
    return {
      page: 0,
      nPages: null,
      itemsPerPage: 8,
      maxPages: 50,
      entries: []
    }
  },
  created () {
    // 「見通しの良いコードの実現」で紹介したコード
  },
   methods: {
    loadPrevPage () {
      this.page -= 1;
    },
    loadNextPage () {
      this.page += 1;
    }
  },
  computed: {
    dispEntries () {
      var startItemIndex = this.page * this.itemsPerPage
      return this.entries.slice(startItemIndex, startItemIndex + this.itemsPerPage)
    }
  }
}
</script>

また、現在表示されているページに応じて「前のページ」を消すことも容易です。button要素にv-if="page !== 0"としておけば、dataオブジェクトの現在のページ数が0になった時、ボタンは自動的に非表示になります。

vue-webpack-boilerplateで開発が便利に

vue-cli」を使用することで、Vueを使用したプロジェクトに必要な各種ファイルが用意され、スピーディーに制作を開始することができました。

プロジェクトのテンプレートはいくつかあるようですが、私は公式に配布されている「vue-webpack-boilerplate」を利用しました。(コマンドラインでvue init webpackを実行するとダウンロードされます。)

vue-webpack-boilerplateを利用して大変便利だったのは、画面の差分更新(Hot Reloading)を実現する「webpack-hot-middleware」が組み込まれていた点です。例えば何かのリストをフィルタ表示している時のビューを作成する場合、Vueのテンプレートを編集して画面が自動リロードされるとまた一から条件を設定してフィルタをしなければなりません。しかし、vue-webpack-boilerplateを利用していれば変更があったところのみを上手く画面に反映してくれるので、一から条件を設定しなおす必要がありませんでした。この差分更新(Hot Reloading)は、開発の大きな助けになるのではないかと感じました。

PostCSSを用いた作業効率化の事例紹介

案件概要

下記のような現状のサイトをスマートフォン対応(マルチスクリーン対応)したいとのご要望で、コーディングのみのご依頼です。

  • 画面上部にグローバルナビ、画面中央の右側にローカルナビを配置したオーソドックスな企業サイト
    • ページ数は100ページ以上
    • ナビゲーションはテキストとCSSでスタイリングされている
  • コンテンツ内容(グループ)毎にCSSが準備されスタイリングされている
    • コンテンツ内容(グループ)内ではモジュールが整備され、ページ間で統一した見栄えを提供している
  • .scssファイル等の提供はなし

対応

サイトを拝見したところ、最近ではレスポンシブWebデザインを採用してマルチスクリーン対応するところをPCのみ対応で制作したという状況のようです。モジュール(コンポーネント)のデザインは今でもよく見かけるものばかりで、マルチスクリーン対応のために全面リニューアルする必要は全くなさそうです。

よって、既存のCSSに手を入れる、具体的にはメディアクエリを利用してスタイル付けを行っていくことにより、さまざまなスクリーンサイズに対応することになります。(もちろん、モジュールのデザインによっては部分的に新たなデザインを検討する必要はあるでしょう。)その際の課題として、以下の2点が考えられます。

  • .cssファイル数が多い
  • .scssファイル等の提供がない

このまま.cssファイルを編集していくか、それとも.scssファイルに変換して編集していくか検討しましたが、どちらも負荷のかかる作業です。そこで、今回はCSSパーサーである「PostCSS」を利用して、既存の.cssを活かしつつ作業の効率化を図ることにしました。

具体的には、以下のPostCSSプラグインを導入しました。

これにより以下の利点がもたらされます。

  • 変数(CSS Custom Properties)がどのファイルでも利用可能になる
  • custom media queriesを利用することでメディアクエリが管理しやすくなる
  • CSSセレクタ毎にメディアクエリを利用してスタイルを記述することができるようになり(既存のセレクタ内にネストして記述できる)、また最終的に「CSS MQPacker」がクエリ毎に設定を集約してくれる
  • 最新のブラウザ環境に合わせてベンダープレフィックスの整理が行われる

まとめ

PostCSSを利用して新しいCSSの仕様や有益な処理を取り入れることにより、容易に作業の効率化を図ることができました。現在のCSS構成を変えることなく利用するので、あるページに設定したスタイルが他のページに悪影響を及ぼし始めた、ということを防ぐことができます。

案件に合わせて最適なツールを選択し利用していくことで、提供する作業の品質を向上させていきたいと考えています。

「HiBiS特別セミナー『逆説のIA/UXデザイン』」に参加して

HiBiS(広島インターネットビジネスソサイエティ) UI/UX研究部会の主催により2017年3月24日(金)に広島市で開催された「HiBiS特別セミナー『逆説のIA/UXデザイン』」に参加してきました。講師は書籍「IA/UXプラクティス モバイル情報アーキテクチャとUXデザイン」などを執筆されたネットイヤーグループ株式会社の坂本 貴史さんでした。

フロントエンド・エンジニアである私ですが、「使う人がうれしいと感じるような体験を実現する製品やサービスを作ることを目指したデザイン方法論(「UXデザインの教科書」より引用)」である『UXデザイン』に興味を持っており、書籍『UXデザインの教科書』を読むなどして基礎知識やプロセスを学んできていました。

そんな私にとって今回のセミナーで坂本さんが紹介された下記3点の逆説は非常に驚きでした。

  • UX をビジネスに導入する必要はない
  • ペルソナを作成する必要はない
  • カスタマージャーニーマップは必要ない

しかしながら、坂本さんはUX導入の目的やプロセス、またペルソナ法やカスタマージャーニーマップなどの手法を分かりやすく解説してくださったことで、前述の逆説を理解することは非常に容易でした。

また、既存のビジネスでは『逆引きのUXデザインプロセス』を用いて現状分析を行うことにより、顧客ロイヤルティの向上・全体最適化を図ることができることを学びました。

カスタマージャーニーマップなどは最近web業界内で流行っている言葉の一つではないかと思いますが、プロセスや手法の本質・視点を忘れることなく利用し、私が接することの多い既にビジネス・サービスを運営されているお客様の課題を解決していくことに注力していきたいと感じました。

講師の坂本さん、UI/UX研究部会長の藤本さん、ありがとうございました。

スタッフの誕生日会。

3月12日生まれのスタッフの誕生日会を3月14日に行いました!

今回はLABの休憩室でホットプレート焼肉。

知人が「油の飛び散りもニオイも少なかった」とホットプレートの写真をFacebookにアップしておられたので「コレだ!」と思い購入。

準備と片付けには少々手間がかかるけれど、好きなものだけ食べられて移動もしなくて良いので、外食よりコスパが良かったです^^

そしてバースデイケーキは、優弥くんが前から何度か美味しいと言っていたベイクドチーズ。

見た目が地味なのでちょっと迷いましたが・・・私も食べてみたかったのでこれにしました^^

濃厚で美味しかったです。

そして恒例の記念撮影。

今年も無事にお祝いできて良かった♡

お問い合わせ Contact

制作のご依頼、ご相談などは下記のフォームからご連絡ください。