Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 16921] ファイルの概要

このコミットは、Go言語の公式ドキュメントサイト(godoc)およびコードウォーク(codewalk)のページ読み込みパフォーマンスを改善することを目的としています。具体的には、JavaScriptファイルのフェッチと実行がページのレンダリングをブロックしないように、スクリプトの読み込みと初期化のメカニズムを変更しています。これにより、ユーザーはより早くページのコンテンツを見ることができるようになります。

コミット

  • コミットハッシュ: d920d8d8493635ff91194a28332b904dbf819214
  • Author: Andrew Gerrand adg@golang.org
  • Date: Tue Jul 30 14:22:14 2013 +1000

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d920d8d8493635ff91194a28332b904dbf819214

元コミット内容

doc: don't block page load on JavaScript fetch

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/12050045

変更の背景

ウェブページのパフォーマンスは、ユーザーエクスペリエンスにおいて非常に重要な要素です。特に、ページの初期表示速度(First Contentful Paint, Largest Contentful Paintなど)は、ユーザーがコンテンツを認識し、操作可能になるまでの時間を決定します。

従来のウェブ開発では、<head>タグ内にJavaScriptファイルを読み込むのが一般的でした。しかし、ブラウザはHTMLを上から順に解析し、<script>タグに遭遇すると、そのスクリプトのダウンロードと実行が完了するまでHTMLの解析とレンダリングを一時停止します。これは「レンダリングブロック」と呼ばれ、特にJavaScriptファイルが大きい場合や、ネットワークが遅い場合に、ページの表示が遅れる原因となります。

このコミットは、Go言語のドキュメントサイトやコードウォークページにおいて、JavaScriptの読み込みがページのレンダリングをブロックする問題を解決するために行われました。具体的には、jQueryやその他の重要なスクリプトの読み込み方法を変更し、ページのコンテンツが先に表示されるようにすることで、ユーザーがより早く情報を得られるように改善を図っています。

前提知識の解説

このコミットの変更内容を理解するためには、以下のウェブ技術と概念に関する知識が役立ちます。

  1. レンダリングブロック (Render Blocking): ブラウザがウェブページをレンダリングする際、HTMLを解析し、CSSを適用し、JavaScriptを実行します。通常、<head>タグ内に配置された<script>タグは、そのスクリプトのダウンロードと実行が完了するまで、ブラウザのHTML解析とレンダリングをブロックします。これにより、ユーザーはスクリプトの処理が完了するまで空白の画面を見ることになり、ページの表示が遅く感じられます。

  2. 非同期JavaScript読み込み: レンダリングブロックを回避するための一般的な手法として、JavaScriptを非同期で読み込む方法があります。

    • スクリプトタグの配置: 最も簡単な方法は、<script>タグを</body>タグの直前に配置することです。これにより、HTMLコンテンツの解析とレンダリングが完了した後にJavaScriptが読み込まれ、実行されます。
    • async属性とdefer属性: <script>タグにasyncまたはdefer属性を追加することで、スクリプトのダウンロードを非同期で行うことができます。
      • async: スクリプトを非同期でダウンロードし、ダウンロードが完了したらすぐに実行します。HTML解析はブロックされませんが、実行時にHTML解析が一時停止する可能性があります。スクリプト間の依存関係がない場合に適しています。
      • defer: スクリプトを非同期でダウンロードし、HTML解析が完了した後に、DOM構築が完了する直前にスクリプトを実行します。スクリプトの実行順序は、HTMLに記述された順序が保証されます。スクリプト間に依存関係がある場合に適しています。
    • 動的なスクリプト要素の作成: document.createElement('script') を使用してJavaScript要素を動的に作成し、DOMに追加することで、スクリプトを非同期で読み込むことができます。
  3. jQuery(document).ready()$(function() {}): これらはjQueryの機能で、DOM (Document Object Model) が完全に構築され、操作可能になった時点で指定された関数を実行するためのものです。通常、ページの初期化処理やDOM操作を行うコードは、このイベントハンドラ内に記述されます。このコミットでは、このjQueryのイベントに依存する代わりに、独自の初期化関数キューを導入しています。

  4. Goのgodoccodewalk:

    • godoc: Go言語の公式ドキュメントツールであり、Goのソースコードからドキュメントを生成し、ウェブサーバーとして提供します。Goの標準ライブラリやサードパーティパッケージのドキュメントを閲覧する際に利用されます。
    • codewalk: Goのコードをステップバイステップで解説するためのツールです。コードの特定のセクションにコメントを付け、それをウェブページ上でインタラクティブに表示することで、コードの理解を助けます。

これらの背景知識を踏まえることで、コミットがなぜ、どのようにしてページのパフォーマンスを改善しているのかを深く理解することができます。

技術的詳細

このコミットは、JavaScriptのレンダリングブロックを解消するために、以下の主要な技術的変更を導入しています。

  1. スクリプトタグの配置変更: lib/godoc/godoc.htmlにおいて、jQuery (jquery.js)、Playground (playground.js)、およびGoドキュメントのスクリプト (godocs.js) の<script>タグを、従来の<head>セクションから</body>タグの直前に移動しました。これにより、ブラウザはHTMLコンテンツを先に解析・レンダリングし、その後にJavaScriptファイルをダウンロード・実行するようになります。

    変更前 (lib/godoc/godoc.html - 抜粋):

    <head>
      ...
      <script type="text/javascript" src="/doc/jquery.js"></script>
      {{if .Playground}}
      <script type="text/javascript" src="/doc/play/playground.js"></script>
      {{end}}
      <script type="text/javascript" src="/doc/godocs.js"></script>
      ...
    </head>
    

    変更後 (lib/godoc/godoc.html - 抜粋):

    <head>
      ...
      <script type="text/javascript">window.initFuncs = [];</script>
    </head>
    <body>
      ...
      <script type="text/javascript" src="/doc/jquery.js"></script>
      {{if .Playground}}
      <script type="text/javascript" src="/doc/play/playground.js"></script>
      {{end}}
      <script type="text/javascript" src="/doc/godocs.js"></script>
    </body>
    

    この変更により、ページのDOM構築がJavaScriptのダウンロードと実行によってブロックされることがなくなります。

  2. カスタム初期化関数キュー window.initFuncs の導入: jQuery(document).ready()$(function() {}) は、DOMが準備できた時点でコードを実行するためのjQueryの機能です。しかし、これらの関数はjQueryがロードされていることを前提としています。スクリプトタグを</body>の直前に移動した場合、jQueryがロードされる前に他のスクリプトが実行される可能性があります。

    この問題を解決するため、godoc.html<head>内でwindow.initFuncs = [];という空の配列を定義しました。そして、doc/codewalk/codewalk.jsdoc/root.htmlで直接jQuery(document).ready()を使用する代わりに、初期化したい関数をこのwindow.initFuncs配列にpushするように変更しました。

    変更前 (doc/codewalk/codewalk.js - 抜粋):

    jQuery(document).ready(function() {
      var viewer = new CodewalkViewer(jQuery('#codewalk-main'));
      viewer.selectFirstComment();
      viewer.targetCommentLinksAtBlank();
    });
    

    変更後 (doc/codewalk/codewalk.js - 抜粋):

    window.initFuncs.push(function() {
      var viewer = new CodewalkViewer(jQuery('#codewalk-main'));
      viewer.selectFirstComment();
      viewer.targetCommentLinksAtBlank();
    });
    

    そして、doc/godocs.js$(document).readyブロック内で、window.initFuncsに格納されたすべての関数をループで実行するようにしました。これにより、jQueryが完全にロードされ、DOMが準備できた後に、すべての初期化処理が順序立てて実行されることが保証されます。

    追加されたコード (doc/godocs.js - 抜粋):

    $(document).ready(function() {
      // ... 既存の初期化処理 ...
    
      // godoc.html defines window.initFuncs in the <head> tag, and root.html and
      // codewalk.js push their on-page-ready functions to the list.
      // We execute those functions here, to avoid loading jQuery until the page
      // content is loaded.
      for (var i = 0; i < window.initFuncs.length; i++) window.initFuncs[i]();
    });
    
  3. インラインスクリプトの外部化と非同期読み込み:

    • Playgroundの初期化ロジックの外部化: lib/godoc/package.htmlにあったPlaygroundの初期化に関するインラインJavaScriptブロックが削除され、そのロジックがdoc/godocs.js内の新しい関数setupInlinePlayground()として移動されました。これにより、HTMLファイル内のJavaScriptコードが減り、コードの保守性が向上します。setupInlinePlayground()は、doc/godocs.js$(document).readyブロック内で呼び出されます。
    • Google+ボタンの非同期読み込み: lib/godoc/godoc.htmlにあったGoogle+ボタン (plusone.js) のインラインスクリプトが削除され、その読み込みロジックがdoc/godocs.js内の新しい関数addPlusButtons()として移動されました。addPlusButtons()関数は、document.createElement('script')を使用してスクリプト要素を動的に作成し、async = trueを設定することで、非同期でplusone.jsを読み込むように変更されました。これにより、Google+ボタンのスクリプトがページのレンダリングをブロックすることがなくなります。

これらの変更は、ウェブページのパフォーマンス最適化における一般的なベストプラクティスに沿ったものであり、ユーザーがGoのドキュメントやコードウォークを閲覧する際の体験を向上させます。

コアとなるコードの変更箇所

このコミットでは、以下のファイルが変更されています。

  • doc/codewalk/codewalk.js:

    • jQuery(document).ready() の呼び出しが window.initFuncs.push() に変更されました。これにより、コードウォークの初期化処理がカスタム初期化キューに追加され、jQueryのロード後に実行されるようになります。
  • doc/godocs.js:

    • 新しい関数 setupInlinePlayground() が追加され、lib/godoc/package.html から移動されたPlaygroundの初期化ロジックをカプセル化しています。
    • 新しい関数 addPlusButtons() が追加され、Google+ボタンのスクリプトを非同期で読み込むロジックをカプセル化しています。
    • $(document).ready() ブロック内で、setupInlinePlayground()addPlusButtons() が呼び出されるようになりました。
    • window.initFuncs 配列に格納されたすべての初期化関数をループで実行するコードが追加されました。
  • doc/root.html:

    • $(function() { (jQueryのショートハンド) の呼び出しが window.initFuncs.push() に変更されました。これにより、ルートページのPlayground初期化処理がカスタム初期化キューに追加されます。
  • lib/godoc/godoc.html:

    • 主要なJavaScriptファイル (jquery.js, playground.js, godocs.js) の<script>タグが<head>から</body>の直前に移動されました。
    • <head>内に window.initFuncs = []; が追加され、カスタム初期化キューが定義されました。
    • Google+ボタンのインラインスクリプトが削除されました。
  • lib/godoc/package.html:

    • Playgroundの初期化に関する大規模なインラインJavaScriptブロックが完全に削除されました。このロジックは doc/godocs.jssetupInlinePlayground() に移動されました。

コアとなるコードの解説

このコミットの核心は、JavaScriptの実行タイミングを制御し、ページのレンダリングをブロックしないようにする新しい初期化メカニズムです。

window.initFuncs の導入と利用

lib/godoc/godoc.html<head> セクションに以下の行が追加されました。

<script type="text/javascript">window.initFuncs = [];</script>

これは、グローバルスコープに initFuncs という空の配列を定義しています。この配列は、DOMが完全にロードされた後に実行されるべき関数を格納するためのキューとして機能します。

次に、doc/codewalk/codewalk.jsdoc/root.html では、従来の jQuery(document).ready()$(function() {}) の代わりに、初期化関数をこの window.initFuncs 配列に push するように変更されました。

doc/codewalk/codewalk.js の変更例:

--- a/doc/codewalk/codewalk.js
+++ b/doc/codewalk/codewalk.js
@@ -296,7 +296,7 @@ CodewalkViewer.prototype.updateHeight = function() {
   this.sizer.height(codeHeight);
 };
 
-jQuery(document).ready(function() {
+window.initFuncs.push(function() {
   var viewer = new CodewalkViewer(jQuery('#codewalk-main'));
   viewer.selectFirstComment();
   viewer.targetCommentLinksAtBlank();

これにより、これらのスクリプトがロードされた時点では関数は実行されず、単にキューに追加されるだけになります。

そして、doc/godocs.js$(document).ready() ブロックの最後に、このキューに格納された関数を順次実行するロジックが追加されました。

doc/godocs.js の追加コード:

$(document).ready(function() {
  // ... 既存の初期化処理 ...

  // godoc.html defines window.initFuncs in the <head> tag, and root.html and
  // codewalk.js push their on-page-ready functions to the list.
  // We execute those functions here, to avoid loading jQuery until the page
  // content is loaded.
  for (var i = 0; i < window.initFuncs.length; i++) window.initFuncs[i]();
});

この構造により、以下の利点が得られます。

  • レンダリングブロックの回避: 主要なJavaScriptファイル(jQueryを含む)が</body>の直前でロードされるため、HTMLの解析とレンダリングがブロックされません。
  • 依存関係の管理: window.initFuncsに登録された関数は、jQueryがロードされ、DOMが準備できた後にgodocs.jsによって実行されるため、jQueryに依存するコードが正しく動作することが保証されます。
  • 初期化処理の一元化: ページの初期化に必要なすべての処理が、このカスタムキューを通じて管理されるようになり、コードの構造がより明確になります。

Playground初期化ロジックの外部化

lib/godoc/package.html にあったPlaygroundの初期化に関するインラインスクリプトは、冗長であり、HTMLとJavaScriptの分離の観点からも好ましくありませんでした。このコミットでは、そのロジックが doc/godocs.js 内の setupInlinePlayground() 関数として外部化されました。

lib/godoc/package.html から削除されたコード (抜粋):

{{if $.Examples}}
<script>
$(document).ready(function() {
	'use strict';
	// Set up playground when each element is toggled.
	$('div.play').each(function (i, el) {
		// ... Playground setup logic ...
	});
});
</script>
{{end}}

doc/godocs.js に追加された setupInlinePlayground() 関数:

function setupInlinePlayground() {
	'use strict';
	// Set up playground when each element is toggled.
	$('div.play').each(function (i, el) {
		// Set up playground for this example.
		var setup = function() {
			var code = $('.code', el);
			playground({
				'codeEl':   code,
				'outputEl': $('.output', el),
				'runEl':    $('.run', el),
				'fmtEl':    $('.fmt', el),
				'shareEl':  $('.share', el),
				'shareRedirect': 'http://play.golang.org/p/'
			});

			// Make the code textarea resize to fit content.
			var resize = function() {
				code.height(0);
				var h = code[0].scrollHeight;
				code.height(h+20); // minimize bouncing.
				code.closest('.input').height(h);
			};
			code.on('keydown', resize);
			code.on('keyup', resize);
			code.keyup(); // resize now.
		};
		
		// If example already visible, set up playground now.
		if ($(el).is(':visible')) {
			setup();
			return;
		}

		// Otherwise, set up playground when example is expanded.
		var built = false;
		$(el).closest('.toggle').click(function() {
			// Only set up once.
			if (!built) {
				setup();
				built = true;
			}
		});
	});
}

この関数は doc/godocs.js$(document).ready() ブロック内で呼び出されます。

$(document).ready(function() {
  // ...
  setupDropdownPlayground();
  setupInlinePlayground(); // ここで呼び出される
  // ...
});

これにより、Playgroundの初期化ロジックがHTMLから分離され、JavaScriptファイル内で一元的に管理されるようになりました。

Google+ボタンの非同期読み込み

Google+ボタンのスクリプト (plusone.js) は、ページの主要なコンテンツとは直接関係のないサードパーティスクリプトです。このようなスクリプトは、ページのレンダリングをブロックしないように非同期で読み込むのがベストプラクティスです。

lib/godoc/godoc.html から削除されたインラインスクリプト:

<script type="text/javascript">
  (function() {
    var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
    po.src = 'https://apis.google.com/js/plusone.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
  })();
</script>

このロジックは doc/godocs.js 内の addPlusButtons() 関数として移動されました。

doc/godocs.js に追加された addPlusButtons() 関数:

function addPlusButtons() {
  var po = document.createElement('script');
  po.type = 'text/javascript';
  po.async = true;
  po.src = 'https://apis.google.com/js/plusone.js';
  var s = document.getElementsByTagName('script')[0];
  s.parentNode.insertBefore(po, s);
}

この関数も doc/godocs.js$(document).ready() ブロック内で呼び出されます。

$(document).ready(function() {
  // ...
  addPlusButtons(); // ここで呼び出される
  // ...
});

この変更により、plusone.js は非同期で読み込まれるため、ページの初期レンダリングに影響を与えることなく、バックグラウンドでダウンロード・実行されます。

これらの変更は、Goのドキュメントサイトのフロントエンドパフォーマンスを大幅に改善し、ユーザーがよりスムーズにコンテンツにアクセスできるようにするための重要なステップです。

関連リンク

参考にした情報源リンク