スポンサーリンク

JavaScriptの動かないコード (中級編) 重いページで,onloadのイベントリスナが実行されない


以下のJavaScriptコードが意図した動作をしないのは,なぜですか。(制限時間1分)

やりたい事:

  • サーバサイドで、画面上のフォーム内に、大量のhiddenデータを描画する。
  • 描画が終わったら、自動的にフォームを送信(submit)する。
<body>

↓このフォームは自動送信されます。
<form id="f">

  (↓100万件のhiddenデータ)
  <% 1.upto( 1000000 ) do |i| %>
    <input type="hidden" id="hoge_<%= i %>" value="<%= i %>">
  <% end %>

</form>


<script>

// 自動的に実行される関数
window.onload = function(){
	// フォームを自動送信する
	document.getElementById("f").submit();
};

</script>


</body>

発生する不具合

IEでは、いつまで経っても画面が切り替わらない場合がある。

待てど暮らせど、onloadのイベントリスナ関数は実行されないかもしれない。



理由

画面の容量が大きすぎる場合、IEでは、

画面の描画終了時にonloadイベントが発生しない場合がある。



HTMLのレンダリング処理があまりに重すぎて、

途中でIEのウィンドウが「応答なし」になり、

そのあとで再度ウィンドウに制御が戻ってくれて、どうにか描画し終わった。


なんて場合には、onloadはスキップされてしまったりする。



それはまだいいほうで、Apache + IEの組み合わせで、

「画面の描画が途中で終わってしまう」

という事態も発生しうる。


HTMLを最後まで出力していないにもかかわらず、

ブラウザの下部に「ページが表示されました」と通知され、

通信が終わってしまうのである。


その結果、HTMLの最後のほうに書いてあるscriptの処理は

ブラウザに読み込まれることすらなく、尻切れトンボのWebページが表示された状態になる。


冒頭のサンプルコードで実験してみたところ、挙動は様々で、予測不能だった。

  • ちゃんと100万まで出力し終えるが、onloadイベントが実行されない。
  • 9000ぐらいで通信がストップしてしまう。onloadも実行されない。

など。



要は、「処理が重すぎて、イベント発生のタイミングを逃してしまうケースがある」

という事のようだ。

この事象は、一種の「setTimeout問題」に分類される、と言えるかもしれない。

javascriptは本当にシングルスレッドで実行されているけれど起こるsetTimeout/prompt現象
http://labs.gmo.jp/blog/ku/2007/09/fi...

  • confirmのときにはconfirmを呼び出したときにイベントがpending状態に変わって後続のイベントが実行されるのに対して、時間のかかる処理のときには実行中のイベントがpending状態に切り替わることがないのでタイムアウトハンドラが呼ばれない。

JavaScriptの動かないコード (中級編) setTimeoutのタイマーが指定時刻に動かないエラー (JavaScriptがマルチスレッドだという誤解)
http://language-and-engineering.hatenablog.jp/entry/20090614/p1

  • alertやpromptなどのモーダル処理が現れると,そこで実行キュー内の後続処理の順番待ちが解除される

ページが読み込まれてからスクリプトを実行する
http://kdimension.system.cx/fwiki/wik...

  • window.onloadはページの画像なども含む全てが読み込まれてからようやく処理されるため、画像が多く重いページでは実行タイミングが遅くなります。
  • 各種JSライブラリではDOMのロード完了を待機する方式が主流

Firefoxでは、IEと比べ格段に処理が軽快で、上記のような問題はまず起こらないようだ。

(ただし、メモリ不足でブラウザが落ちて「Mozillaクラッシュレポータ」が立ち上がる可能性はある。)


回避策

サーバサイドの異なるプログラミング言語間でCGIを連携させたい(PHPからRubyにPOSTとか)

なんて場合、サーバ内部で情報のやり取りをするのが面倒だ。

だから、どうしてもformを使って画面間で情報を持ちまわりたい誘惑に駆られる。



しかし一般論として、Webアプリの設計方針において

「1000件を超える大量のhidden要素を使って、画面間で情報を持ちまわる」

という発想をやめよう。



持ちまわりたいデータの容量がそこまで大きい場合、

そのデータを直接セッションに格納することも危なくなってくる。

セッションに関する一般的なガイドライン
http://www.flatz.jp/archives/2266

  • セッションに大きなオブジェクトを保存しない。かわりに、データベースにオブジェクトを保存して、そのIDをセッションに保存するべきです。

RailsでsessionをDBに入れるとき気を付ける点
http://d.hatena.ne.jp/saekik/20100329...
大きすぎるオブジェクトを入れるとエラーになる


セッションではなく、専用の「情報を持ちまわるための一時的なテーブル」をDB内に準備し、

ブラウザ上での画面切り替えがスピーディーに処理されるように設計したほうがよいだろう。


補足

もしサーバサイドスクリプトに、スクリプトの実行時間の上限(タイムアウト)が設定されている場合、

上で述べたような問題はやはり発生しうる。


たとえばサーバ上でRubyを単独で動かしている場合、

RubyTimeOutという設定パラメータによって各スクリプトの実行時間の上限が決まる。

それ以上時間がかかる場合、処理は強制的に中断されてしまい、未完成の画面が出力されることになる。

mod_ruby Apacheディレクティブ
http://www.modruby.net/doc/directives...

  • RubyTimeOut 60
  • タイムアウト時間が過ぎても実行されているスクリプトは強制的に終了される。サーバ設定のみに指定可。

また、これとは別に、Webサーバにはやはり「タイムアウト時間」の設定値がある。


しかしApacheの場合、Timeoutディレクティブの意味するところは、

「処理にかかった時間の上限」ではない。

Timeoutパラメータの意味は、「(CGI等の)出力に待ち時間があった場合、何秒まで待つか」である。

だから、かりにTimeoutの設定値が300であった場合、

CGIが299秒おきに1文字ずつ文字を出力すれば、それはタイムアウトとはみなされないのである。

CGI のタイムアウトおよびブラウザの中断
http://www.bioinfo.jp/tips.html#timeout

  • Apache は CGI スクリプトからの出力が一定時間ないとタイムアウトとみなし、クライアントとの接続を終了し、CGIスクリプトを殺しにかかる。

長い処理をしているとタイムアウトしてしまいます。処理が終わるまで、タイムアウトさせない方法をどなたか教えてください。
http://q.hatena.ne.jp/1174063743

  • Apache の Timeout ディレクティブ を長くする。
  • もしくは処理が終わるまで、ダミーの空白などを少しづつ出し続ける事でタイムアウトさせない。

リクエスト処理時間が数時間? - HTTPサーバのタイムアウト設定について -
http://dev.ariel-networks.com/Members...

  • ただ5分(300秒)スリープするスクリプト
  • httpd.confでTimeout値を30(秒)にすると、30秒後にWebブラウザがエラー画面になります。ステータスコードは500

Apacheのタイムアウト発生メカニズム
http://www.atmarkit.co.jp/bbs/phpBB/v...

  • 送った要求に対して一定時間肯定も否定もなかった場合をタイムアウトと呼びます。

つまり、「サーバ側で処理に時間がかかっていても、何か出力され続けていれば、それはApache的にはタイムアウトにはしない」ということ。


だから、Apacheの観点で言えば、今回冒頭で取り上げたサンプルコードには問題はない。

絶えずHTML部品を出力し続けているので、Apache的にはタイムアウトにならないのだ。


結論として、Apacheには問題ない、ということがわかる。



さらに補足として、Webサーバのタイムアウト設定とは別に、「KeepAliveのタイムアウト設定」というものもある。

この設定パラメータは、サーバ側にも、ブラウザ側にもある。

このパラメータは、「サーバとクライアントの間で、どこまで古いTCP接続を再利用するか?」を規定する。


ApacheとKeep Aliveの実験
http://slashdot.jp/~noriyuru/journal/...

  • Keep AliveはHTTP/1.1で定められたもので、1度の接続で複数のリクエストと、その応答を行うことを目的としてる。
  • IEとFirefoxは常にkeep-alive。たぶん、世界中のほとんどのブラウザーは常にkeep-aliveを出す。
  • クライアント側は接続を分割する場合がある。これは1クライアントが1接続にならないことを意味する。つまり、Keep Alive設定でリクエストを待ち続けても利用されない場合がある。


Internet Explorer のデフォルトの Keep-Alive タイムアウト値を変更する方法
http://support.microsoft.com/kb/81382...

  • Internet Explorer が、(Connection: Keep-Alive ヘッダを使用して) Web サーバーに永続的な HTTP 接続を確立した場合、Internet Explorer は、最初の要求の受信に使用したソケットと同一の TCP/IP ソケットを、そのソケットが 1 分間アイドルになるまで再使用します。
  • 接続が 1 分間アイドルだった場合、Internet Explorer はその接続をリセットします。その後の要求受信には新しい TCP/IP ソケットが使用されます。
  • Internet Explorer の HTTP KeepAlive タイムアウト値は変更することができます。

上記URLを見る限りでは、IEがサーバとの間で再利用を許容する接続の古さは、1分間まで。


という事はもしかすると、

何らかの理由(Webページが重いとか)によってIEが1分間「固まって」しまった場合、

IEはkeep-aliveタイムアウトによって既存の接続を破棄し、

その辺が何か悪さをして、IEがWebページの表示を途中でやめてしまうのかもしれない。



巨大Webページの正体が静的コンテンツなら大丈夫かもしれないが、

動的に描画されるページの場合は、HTTPレスポンスにContent-Lengthも付けようがないし、

ブラウザ側の自主的な判断で勝手に通信を終えてしまうのでは。

ContentLengthを取得するには
http://rararahp.cool.ne.jp/cgi-bin/ln...

  • プログラムで動的に作られるページは完成前に正確なサイズを知ることができないので,Content-Lengthを付けられない。
  • 静的ファイルはWebサーバーがサイズを調べて自動でContentLengthを付けてくれる。

mod_deflateで動的コンテンツにContent-Length レスポンスヘッダを追加する
http://d.hatena.ne.jp/hogem/20100716/...

  • 携帯向けではContent-Lengthが必須ヘッダ(docomoとsoftbank)となっているけど、cgi/ssiだと (cgiはアプリ側で記述していなければ) Content-Lengthヘッダは追加されない。でもmod_deflateで圧縮すればapacheが勝手に追加してくれる

HTTP Header Fields
http://www.studyinghttp.net/header

  • Content-Length ヘッダが送られない場合は、接続の終了を持ってエンティティボディの終末を判断する事ができる

詳しいことはMSの中の人しか知るまいが。



さらにおまけの補足をすると、

冒頭のコードではRubyの Fixnum#upto() メソッドを使ってループしているが、

あえて禁じ手の for ループを使って下記のように書き換えると、

IEでもすんなり画面が動いてくれたりする。

  (↓100万件のhiddenデータ)
  <% for i in 1..1000000 do %>
    <input type="hidden" id="hoge_<%= i %>" value="<%= i %>">
  <% end %>

定石としてイテレータを使うべきところで、あえて掟破りをする事により、

パフォーマンス上の問題が解決されたりする。


しかし当然ながら、このケースは設計が悪いのであって、for文を使ってもよい理由にはならない。