anti scroll

ブラウザと小説の新しい関係を模索する

シリーズから作品を簡単に削除できるようになりました

今までシリーズから作品を削除する際には、作品一覧の画面から「シリーズの設定」を選んで、選択されているシリーズを解除する、という(面倒な)操作が必要でした。

シリーズの設定を選んで、登録先を無効化する、という手順が必要だった

今後はシリーズ一覧画面から、それぞれのシリーズの「シリーズの並び替え・編集」を選ぶと、

シリーズの並び替えと編集ボタン

シリーズに登録されたそれぞれの作品の横に「削除ボタン」が表示されるようになったので、そこから作品を当該シリーズから削除できるようになりました。

削除ボタンが追加された

もちろんシリーズから作品を削除するだけで、作品そのものが削除されるわけではないことにご注意ください。

作品ページからシリーズ内の別の作品に移動できるようになりました

小さな修正ですが、作品のビューアー下部に表示されるシリーズ情報から、以下のようにシリーズ内の別の作品に移動できるようになりました(これまではシリーズページをわざわざ開く必要がありました)。

ドロップダウンからシリーズ内の作品に移動できる

ぜひ使ってみて下さい。

青空文庫の作品をシリーズにまとめました(Part2)

以前、青空文庫の作品をシリーズにまとめた、という記事を書きました。

tategakibunko.hatenablog.com

今回はその第二弾です(ちなみに、鏡の国のアリス青空文庫ではなく、プロジェクト杉田玄白の作品です)。

余談ですが、谷崎潤一郎の「細雪」って「ささめゆき」って読むんですね…

タイトル、筆者名、タグなどで作品が検索できるようになりました

縦書き文庫内の作品が、タイトル名、筆者、タグなどで検索できるようになりました。表示順序は、人気順と新着順が選べます。

これまで縦書き文庫内の検索はグーグルさんの検索に任せていたのですが、最近グーグルさんの検索結果から縦書き文庫のページが(たぶん当方の技術的なミスにより)消えてしまい、今も復旧の目処が立たないので、急遽ですが自前の検索システムを実装しました。

人気順と新着順を選べる

実際に「タイトル」で「漱石」という単語を「人気順」で検索するとこんな感じです。

漱石、で検索

右上の検索窓から検索してもいいですし、

右上の検索窓

直接、検索ページに行ってもOKです。

ページを指定して作品を開くことができるようになりました

ページ指定の方法

作品ページのURLの末尾に?p={ページ番号}を付けると、そのページ番号から作品を開けるようになりました。

例えば、夏目漱石の「こころ」の10ページ目を開くリンクは、次のようになります。

https://tb.antiscroll.com/novels/library/6162?p=10

「管理ページ > しおり」ページの改善

「管理ページ > しおり」で表示される、それぞれのリンクをクリックすると、しおりを挟んだページから再開されるようになりました。

これまでそうじゃなかったのが変だったのですが。。。

注意事項

ちなみにページを指定したリンクを他人と共有するのは、オススメできません。

なんでかというと、ページの縦横サイズは、端末の解像度によって変わるからです。

例えばスマホでは、1ページのサイズが、PCに比べて(とても)小さくなりますよね。

なので、PCで2ページ目だった内容が、スマホだと10ページ目ぐらいだったり、みたいなことになります。

というわけで、ツイッターとかで「このページを読んで!」みたいな感じで引用リンクを貼っても、あんまり意味がないことに注意してください。

作品の長さに関係なく、高速に作品が表示されるようになりました

非同期処理への対応

少し前にブログで「wasmではjsと非同期のやり取りをするのが難しい」と書いたのですが、この技術的な課題をなんとか解決できたので、組版の完了したページを(全ページの計算の完了を待たずに)表示できるようになりました。

これによって、すべての作品が0.1 ~ 0.2秒ぐらいで表示されるようになり、大幅に使用感が改善されたと思います。

技術的な部分

実はjsと非同期にやり取りする部分のコードのサンプルは、wasm_bindgenのサンプルに入っています。

https://github.com/rustwasm/wasm-bindgen/tree/main/examples/request-animation-frame

肝要なところを抜き出すと以下のようになります。

pub fn run() {
  let f = Rc::new(RefCell::new(None));
  let g = f.clone();
  let mut i = 0;

  *g.borrow_mut() = Some(Closure::new(|| {
    if i >= 300 {
      let _ = f.take();
      return;
    }
    i += 1;
    request_animation_frame(f.borrow().as_ref().unwrap());
  }));
  request_animation_frame(g.borrow().as_ref().unwrap());
}

fgはともにクロージャーを保持する参照カウント付きのポインタなのですが、gは最初にクロージャーを走らせるためだけに使います。

grunの終了後にクロージャーの参照カウントを減らしますが、もう一方のポインタfクロージャーの中で再帰的に参照され続けるため、何もしないとこのクロージャーに対する参照カウントは永遠にゼロにならず、解放されなくなってしまいます。

なのでreqest_animation_frameの処理を抜けるタイミングで、中身をf.take()で取り出し、スコープを抜けたところで解放されるようにしているわけですね。

基本は理解できるのですが、nehanの場合、上のサンプルにおけるクロージャーの自由変数が、もうちょっと複雑なデータになっていまして、これだけだと動かない部分があり、ハマっていたのです…

しかしまあ、色々と頑張ったら、なんとか動くようになったので、無事に縦書き文庫のビューアーが(以前と同じく)非同期にページを表示できるようになりました。

ちなみに上のrequest_animation_frameはもちろんjsの関数ではなく、rust側のweb_sysパッケージが提供している関数でして、たぶんですがrust側のデータをjs側のデータにいちいち「お直し」する処理が含まれると思われるため、あんまりたくさん呼ぶのはよくなさそうです。

なので、縦書き文庫の場合は、数十ページぐらい組版し、まとめて送信することで、jsとrust間の通信回数を減らすようにしています。

導入の成果

非同期処理に切り替えた結果、全ページを組版するのにかかる時間は、一度にすべて組版してから送信する方式よりは(当たり前ですが)少しだけ遅くなりました。

しかし、せいぜい5% ~ 10%ぐらいの遅延なので、許容範囲だと思います。

なにより作品を開いてから「しばらくお待ち下さい」の表示がほとんどなくなり、一瞬で作品が表示されるメリットのほうが遥かに大きいです。

読者の離脱率も、大きく改善されるものと思われます。

WebAssembly導入の効果をプラットフォーム別に比較してみた

先日、縦書き文庫組版エンジン(nehan)をRustで書き直し、WebAssembly化したと報告しました。

tategakibunko.hatenablog.com

その際に「約3倍の速度向上があった」と書いたのですが、あれから約2週間分の利用者のログをもとに、プラットフォーム別に速度を比較したところ、ちょっと違う結果が出ました。

比較したプラットフォーム

とても大雑把ですが、PC(Windows)とPC(Mac)とiPhoneAndroidに分けて計測しました。

プラットフォーム別の速度

以下は速度比較のグラフです。縦軸は秒速の組版字数で、大きいほど速いことになります。

赤がnehan7(js)の速度で、青がnehan8(wasm)の速度です。

nehan7(js) vs nehan8(wasm)

プラットフォーム別の速度向上率は、こんな感じでした。

結果について

メモリが潤沢と思われるPCにおいては、WindowsMac組版速度が秒速30万字を超えているので、大抵の作品で1秒以内に組版が完了し、ストレスなく作品が表示されるものと思われます。

iPhoneも秒速16万字ぐらいの速度が出ているようなので、長い作品では2〜3秒は待たされてしまうかもしれませんが、概ねストレスなく表示されそうです。

最後にAndroidですが、これは古い機種(Android 10とか)のスマホが平均値を押し下げているみたいで、秒速7万字ぐらいになってしまいました(統計を取るときに、Android 12以降、みたいな縛りをつけたら良かったかもしれません)。

ちなみに秒速7万字ぐらいだと、40万字超えの作品(例えば夏目漱石の「こころ」とか)は5〜6秒ぐらい待たされることになり、そこそこストレスだと思います。

しかしそれでもjsよりは約8倍速いので、WebAssmbly化の効果は大きかったと言えるでしょう。

その他のプラットフォームでも、だいたい6〜9倍の速度向上があり、概ね満足な結果と言えます。

これでwasmからjsに非同期にデータを送信する方法があったら、もっと快適な読者体験を提供できるのですが…

nehan(縦書き文庫の組版エンジン)をWebAssembly化することで、約3倍の高速化を達成しました

縦書き文庫組版エンジンであるnehan(js製)をRustで書き換え、WebAssemblyで実行したところ、約3倍の高速化に成功しました。

現時点ですでに運用されています。

感想としては「本当は10倍ぐらい速くなって欲しかったけど、そこまでは速度が出ずにトホホ…」という感じです。

3倍なら良いではないか、と思われる方もいるかもしれませんが、青空文庫の長編小説なんかは、だいたい40万字ぐらいあり、そのjsでの組版時間は(すごく遅い端末だと)27秒に達することもあります。

ちなみに手元のノートPC(メモリ8G)でjsに組版させると5秒ぐらいです。

この5秒が1.7秒ぐらいになるのは嬉しくても、27秒が9秒になっても、あんまり嬉しくないですよね。

だから、ずっと10倍を目標にしてきたのですが…

まあこれから頑張って、Rust側のソースを最適化していこうと思います。

ビューアーの変更点

  • 全ページの計算が3倍速くなった
  • 作品を開いたとき、自動で前回に開いたページまで移動するようになった
  • 全ページ計算してから表示されるようになった(以前は先頭ページが計算できたら、すぐに表示されていた)
  • 数式の表示やコードハイライトの機能は削除した

注意してほしいのは、全ページの計算が終わるまで、先頭ページが表示されない、という新しい仕様です。

(2022/09/10 追記:解決しました!)

作品の長さに関係なく、高速に作品が表示されるようになりました - anti scroll

これが問題になるのは、文字数のすごく多い作品(100万字以上とか)を、すごく遅い端末(古いスマホとか)で開いた場合です。

古い組版エンジンだと、たとえ遅い端末でも、とりあえず先頭ページ「だけ」は高速に表示されたため、そんなに「待たされた感」はなかったと思います。

しかし新しいビューアーだと、全ページの計算が終わらないと作品が表示されないので、遅い端末だと待たされ感が凄いことになりそうです。

なんでこんな作りになってしまったかというと、ウェブアプリ(js)と組版エンジン(wasm)の間で、非同期にデータをやり取りするのが難しいからです。

正確には、 js側のデータを非同期でwasmに送信することはできそうですが、wasm側のデータを非同期でjsに送信するのが難しいのです。

以前の組版エンジンであるnehan7(js製)では、全体の組版速度は遅くとも、先頭ページが組版できたら、さっさとアプリ側に表示させることができました。

どっちもjsなので、非同期なデータのやり取りがしやすかったのです。

読者が先頭ページを読んでいる間、裏でこっそり残りページを計算し続ける、みたいなこともできました。

しかしWebAssemblyだと「ちょっとずつ計算した結果を、ちょっとずつアプリ側(js)に渡す」みたいな非同期処理が難しいので、全ページ計算してjsに渡すしかありません。

今後の展望

とまあ、いくつか頭の痛い問題はありますが、しばらく新しいエンジンを運用してみます。

とりあえず、新し目のスマホやPCで開くぶんには、ほとんどのケースで快適になるのも事実ですので。

nehan8のコードは、いずれGithubに公開したいと思っていますが、時期はもうちょい先になりそうです。

というのも、仮にソースを公開したところで、中身でRustの実験的な機能を使っている関係上、普通のRustコンパイラではコンパイルできないからです(最新の開発者版コンパイラが必要)。

そのへんのことで無用な混乱を生むのも面倒なので、Rust側が安定してから公開することにします。

開発後記

nehanの大きなバージョンアップは今回で8回目になりますが、今回ばかりは本当に心が折れそうでした。

いつも組版エンジンの書き直しは地獄なのですが、今回は言語がRustに変わったので、なにもかもが新しいことだらけで、しんどかったです。

去年の8月から着手して、今年の7月末に完了ですから、まるまる1年かかった計算になります。

jsからTypeScriptにしたときですらしんどかったですが、今回はその比じゃありませんでした。

ただRustは良い言語だなと思いました。慣れるまでは大変でしたが。少なくともC++よりはずっと書きやすい言語だと思います。