🌝ただJSONサイズを80%削減する話2025/01/20に公開2025/01/227件WebperformancefrontendfrontendパフォーマンスtechDiscussionあいや - aiya0002025/01/21に更新 このJSONの圧縮処理は同期的な処理なので async functionやPromiseの中で実行をすればいい、という単純な話ではない感じですかね? mpyw2025/01/24に更新前提として JavaScript 実行環境は(クライアント側・サーバ側ともに)原則的にはシングルスレッド動作です。つまり単一の CPU コアしか利用できません。 Promise は IO バウンドな処理の多重化・平行化(並列化ではありません) に使われるだけです。複数 CPU コアを別々のタスクに割り当てるということはできず、単一 CPU コア上であるタスクの IO の待ち時間に別のことをやってもらう ということしか出来ません。 そのため、今回のような CPU バウンドな処理は Promise ベースの提供になっていないことが殆どで、そういった処理は明示的にプロセスやスレッドを起こさないと複数の CPU コアを利用した真の並列化は実現できません。 あいや - aiya0002025/01/22に更新 つまり単一の CPU コアしか利用できません。 この記事ではスクレイピングの文脈とのことで、少なくともNode.jsではデフォルトで(必要に応じて)マルチスレッドが使用されるはずと思っています👀 なんとなくわかりやすそうなページ↓ https://qiita.com/darai0512/items/568ea7d49d2c522b7c45 マルチスレッドはOSによって複数のCPUコアが使われる場合がある認識ですが、ここが間違っているのでしょうか? Promise は IO バウンドな処理の多重化・平行化(並列化ではありません) に使われるだけです 僕が引用した内容は「JSON圧縮が同期処理である」というもので、多重化・並行並列処理に関しては言及していないつもりです🙋♂️ またPromiseで行う処理が同期処理であるか非同期処理か、については非同期処理である、と考えているのですが、そこが間違っていますか? あいや - aiya0002025/01/23に更新話題に上げたらありがたいことにアドバイスをいただいたので、一応こちらにも上げておきます🙆♂️ もし興味がある方は、リプライツリーを見ていただけると。 https://x.com/public_ai000ya/status/1881959017574256836?t=8mhwKBGuRduu9-IANOQi9g&s=19 mpyw2025/01/24に更新少々語弊があったみたいですね,すみません。 Promise は IO バウンドな処理の多重化・平行化(並列化ではありません) に使われるだけです。 こちらは語弊があったので無視してください。これが正しいかどうかは内包する処理に依存する,というのが正しい説明でした。 以下,仕切り直して説明します。具体例を挙げて,各ケースにおける「並行性」と「並列性」について説明します。 JSON.stringify で大きなペイロードを文字列化することを想定してください。 A. JSON.stringify をラップした JavaScript の同期関数 コード例 const syncStringify = (data) => JSON.stringify(data); 説明 並行性: ❌ 並列性: ❌ B. JSON.stringify をラップした JavaScript の Promise 関数 コード例 const concurrentStringify = async () => JSON.stringify(data); const concurrentStringify = (data) => new Promise((resolve) => { resolve(JSON.stringify(data)); }); 説明 並行性: 🔺 セマンティック的にはありに分類されるが,実際のところは無しに近い。 内部の処理が全て同期実行であるため,一度 concurrentStringify の実行に入ると全ての処理が終わるまでこの関数を抜けることはない。 並列性: ❌ C. JSON.stringify を child_process 上で実行するようにラップした Node.js の Promise 関数 コード例 import { fork } from 'node:child_process'; import path from 'node:path'; const parallelStringify = (data) => new Promise((resolve, reject) => { const child = fork(path.resolve(__dirname, 'stringify-worker.js')); child.on('message', (result) => resolve(result)); child.on('error', reject); child.send(data); }); stringify-worker.js: process.on('message', (data) => { const result = JSON.stringify(data); process.send(result); }); 説明 並行性: ✅ 並列性: ✅ child_process を使用することで別のプロセスで処理され,CPUのコアを活用した並列実行が可能 D. fs.readFile をラップした Node.js の Promise 関数 コード例 import * as fs from 'node:fs/promises'; const asyncReadFile = async (filePath) => { return await fs.readFile(filePath, 'utf8'); } import * as fs from 'node:fs'; const asyncReadFile = (filePath) => new Promise((resolve, reject) => { fs.readFile(filePath, 'utf8', (err, data) => { err ? reject(err) : resolve(data); }); }); 説明 並行性: ✅ 並列性: ✅ fs.readFileはlibuvのスレッドプールを利用するため,複数の IO 操作を並列で処理可能 もとの話に戻りますが,A から B のように書き換えたところで, 「同期処理として提供されているものを Promise でラップしただけでは何も解決しないよ」 が私の一番いいたいところでした。 Promise でラップするだけで OK というのは, D のように元から並列性が実現されているものを取り扱う場合のみであり,そうなっていない処理の場合は基本的には C のような対応が必要ということです。 Node.js であれば child_process など(他にも選択肢あり)を使用します。 Web ブラウザであれば WebWorker を使用します。 返信を追加nawada2025/01/22 1ファイル圧縮するのに10分以上かかる 単純に気になったのですが、10秒じゃなく10分ですか…?ファイルがすごく大きいのでしょうか? nuko_suke_dev2025/01/22に更新あー、すみません、10分なのはCloud Buildのデフォルトの machineType で実行した結果です!ので実行する環境によってはもっと速くなるかと思います〜 ちなみに10分くらいかかってるファイルのサイズは大体100kB~200kBです。 なお、JSONで記述されている各オブジェクトは、オブジェクトや配列がネストされたような複雑なものでもなく、valueが文字列や数値の単純な形式のオブジェクトです。 簡易的ですがコード例も貼っておきます。 import fs from 'node:fs/promises'; import { compress } from 'compressed-json'; import JSONCrush from 'jsoncrush'; function heavyCompressJsonToString(value) { return JSONCrush.crush(JSON.stringify(compress(value))); } export async function writeDataToLocalFile(exportDir, data) { const t0 = performance.now(); await fs.writeFile(exportDir, heavyCompressJsonToString(data)); console.log(`Write ${performance.now() - t0} ms`); } 返信を追加
あいや - aiya0002025/01/21に更新 このJSONの圧縮処理は同期的な処理なので async functionやPromiseの中で実行をすればいい、という単純な話ではない感じですかね? mpyw2025/01/24に更新前提として JavaScript 実行環境は(クライアント側・サーバ側ともに)原則的にはシングルスレッド動作です。つまり単一の CPU コアしか利用できません。 Promise は IO バウンドな処理の多重化・平行化(並列化ではありません) に使われるだけです。複数 CPU コアを別々のタスクに割り当てるということはできず、単一 CPU コア上であるタスクの IO の待ち時間に別のことをやってもらう ということしか出来ません。 そのため、今回のような CPU バウンドな処理は Promise ベースの提供になっていないことが殆どで、そういった処理は明示的にプロセスやスレッドを起こさないと複数の CPU コアを利用した真の並列化は実現できません。 あいや - aiya0002025/01/22に更新 つまり単一の CPU コアしか利用できません。 この記事ではスクレイピングの文脈とのことで、少なくともNode.jsではデフォルトで(必要に応じて)マルチスレッドが使用されるはずと思っています👀 なんとなくわかりやすそうなページ↓ https://qiita.com/darai0512/items/568ea7d49d2c522b7c45 マルチスレッドはOSによって複数のCPUコアが使われる場合がある認識ですが、ここが間違っているのでしょうか? Promise は IO バウンドな処理の多重化・平行化(並列化ではありません) に使われるだけです 僕が引用した内容は「JSON圧縮が同期処理である」というもので、多重化・並行並列処理に関しては言及していないつもりです🙋♂️ またPromiseで行う処理が同期処理であるか非同期処理か、については非同期処理である、と考えているのですが、そこが間違っていますか? あいや - aiya0002025/01/23に更新話題に上げたらありがたいことにアドバイスをいただいたので、一応こちらにも上げておきます🙆♂️ もし興味がある方は、リプライツリーを見ていただけると。 https://x.com/public_ai000ya/status/1881959017574256836?t=8mhwKBGuRduu9-IANOQi9g&s=19 mpyw2025/01/24に更新少々語弊があったみたいですね,すみません。 Promise は IO バウンドな処理の多重化・平行化(並列化ではありません) に使われるだけです。 こちらは語弊があったので無視してください。これが正しいかどうかは内包する処理に依存する,というのが正しい説明でした。 以下,仕切り直して説明します。具体例を挙げて,各ケースにおける「並行性」と「並列性」について説明します。 JSON.stringify で大きなペイロードを文字列化することを想定してください。 A. JSON.stringify をラップした JavaScript の同期関数 コード例 const syncStringify = (data) => JSON.stringify(data); 説明 並行性: ❌ 並列性: ❌ B. JSON.stringify をラップした JavaScript の Promise 関数 コード例 const concurrentStringify = async () => JSON.stringify(data); const concurrentStringify = (data) => new Promise((resolve) => { resolve(JSON.stringify(data)); }); 説明 並行性: 🔺 セマンティック的にはありに分類されるが,実際のところは無しに近い。 内部の処理が全て同期実行であるため,一度 concurrentStringify の実行に入ると全ての処理が終わるまでこの関数を抜けることはない。 並列性: ❌ C. JSON.stringify を child_process 上で実行するようにラップした Node.js の Promise 関数 コード例 import { fork } from 'node:child_process'; import path from 'node:path'; const parallelStringify = (data) => new Promise((resolve, reject) => { const child = fork(path.resolve(__dirname, 'stringify-worker.js')); child.on('message', (result) => resolve(result)); child.on('error', reject); child.send(data); }); stringify-worker.js: process.on('message', (data) => { const result = JSON.stringify(data); process.send(result); }); 説明 並行性: ✅ 並列性: ✅ child_process を使用することで別のプロセスで処理され,CPUのコアを活用した並列実行が可能 D. fs.readFile をラップした Node.js の Promise 関数 コード例 import * as fs from 'node:fs/promises'; const asyncReadFile = async (filePath) => { return await fs.readFile(filePath, 'utf8'); } import * as fs from 'node:fs'; const asyncReadFile = (filePath) => new Promise((resolve, reject) => { fs.readFile(filePath, 'utf8', (err, data) => { err ? reject(err) : resolve(data); }); }); 説明 並行性: ✅ 並列性: ✅ fs.readFileはlibuvのスレッドプールを利用するため,複数の IO 操作を並列で処理可能 もとの話に戻りますが,A から B のように書き換えたところで, 「同期処理として提供されているものを Promise でラップしただけでは何も解決しないよ」 が私の一番いいたいところでした。 Promise でラップするだけで OK というのは, D のように元から並列性が実現されているものを取り扱う場合のみであり,そうなっていない処理の場合は基本的には C のような対応が必要ということです。 Node.js であれば child_process など(他にも選択肢あり)を使用します。 Web ブラウザであれば WebWorker を使用します。 返信を追加
mpyw2025/01/24に更新前提として JavaScript 実行環境は(クライアント側・サーバ側ともに)原則的にはシングルスレッド動作です。つまり単一の CPU コアしか利用できません。 Promise は IO バウンドな処理の多重化・平行化(並列化ではありません) に使われるだけです。複数 CPU コアを別々のタスクに割り当てるということはできず、単一 CPU コア上であるタスクの IO の待ち時間に別のことをやってもらう ということしか出来ません。 そのため、今回のような CPU バウンドな処理は Promise ベースの提供になっていないことが殆どで、そういった処理は明示的にプロセスやスレッドを起こさないと複数の CPU コアを利用した真の並列化は実現できません。
あいや - aiya0002025/01/22に更新 つまり単一の CPU コアしか利用できません。 この記事ではスクレイピングの文脈とのことで、少なくともNode.jsではデフォルトで(必要に応じて)マルチスレッドが使用されるはずと思っています👀 なんとなくわかりやすそうなページ↓ https://qiita.com/darai0512/items/568ea7d49d2c522b7c45 マルチスレッドはOSによって複数のCPUコアが使われる場合がある認識ですが、ここが間違っているのでしょうか? Promise は IO バウンドな処理の多重化・平行化(並列化ではありません) に使われるだけです 僕が引用した内容は「JSON圧縮が同期処理である」というもので、多重化・並行並列処理に関しては言及していないつもりです🙋♂️ またPromiseで行う処理が同期処理であるか非同期処理か、については非同期処理である、と考えているのですが、そこが間違っていますか?
あいや - aiya0002025/01/23に更新話題に上げたらありがたいことにアドバイスをいただいたので、一応こちらにも上げておきます🙆♂️ もし興味がある方は、リプライツリーを見ていただけると。 https://x.com/public_ai000ya/status/1881959017574256836?t=8mhwKBGuRduu9-IANOQi9g&s=19
mpyw2025/01/24に更新少々語弊があったみたいですね,すみません。 Promise は IO バウンドな処理の多重化・平行化(並列化ではありません) に使われるだけです。 こちらは語弊があったので無視してください。これが正しいかどうかは内包する処理に依存する,というのが正しい説明でした。 以下,仕切り直して説明します。具体例を挙げて,各ケースにおける「並行性」と「並列性」について説明します。 JSON.stringify で大きなペイロードを文字列化することを想定してください。 A. JSON.stringify をラップした JavaScript の同期関数 コード例 const syncStringify = (data) => JSON.stringify(data); 説明 並行性: ❌ 並列性: ❌ B. JSON.stringify をラップした JavaScript の Promise 関数 コード例 const concurrentStringify = async () => JSON.stringify(data); const concurrentStringify = (data) => new Promise((resolve) => { resolve(JSON.stringify(data)); }); 説明 並行性: 🔺 セマンティック的にはありに分類されるが,実際のところは無しに近い。 内部の処理が全て同期実行であるため,一度 concurrentStringify の実行に入ると全ての処理が終わるまでこの関数を抜けることはない。 並列性: ❌ C. JSON.stringify を child_process 上で実行するようにラップした Node.js の Promise 関数 コード例 import { fork } from 'node:child_process'; import path from 'node:path'; const parallelStringify = (data) => new Promise((resolve, reject) => { const child = fork(path.resolve(__dirname, 'stringify-worker.js')); child.on('message', (result) => resolve(result)); child.on('error', reject); child.send(data); }); stringify-worker.js: process.on('message', (data) => { const result = JSON.stringify(data); process.send(result); }); 説明 並行性: ✅ 並列性: ✅ child_process を使用することで別のプロセスで処理され,CPUのコアを活用した並列実行が可能 D. fs.readFile をラップした Node.js の Promise 関数 コード例 import * as fs from 'node:fs/promises'; const asyncReadFile = async (filePath) => { return await fs.readFile(filePath, 'utf8'); } import * as fs from 'node:fs'; const asyncReadFile = (filePath) => new Promise((resolve, reject) => { fs.readFile(filePath, 'utf8', (err, data) => { err ? reject(err) : resolve(data); }); }); 説明 並行性: ✅ 並列性: ✅ fs.readFileはlibuvのスレッドプールを利用するため,複数の IO 操作を並列で処理可能 もとの話に戻りますが,A から B のように書き換えたところで, 「同期処理として提供されているものを Promise でラップしただけでは何も解決しないよ」 が私の一番いいたいところでした。 Promise でラップするだけで OK というのは, D のように元から並列性が実現されているものを取り扱う場合のみであり,そうなっていない処理の場合は基本的には C のような対応が必要ということです。 Node.js であれば child_process など(他にも選択肢あり)を使用します。 Web ブラウザであれば WebWorker を使用します。
nawada2025/01/22 1ファイル圧縮するのに10分以上かかる 単純に気になったのですが、10秒じゃなく10分ですか…?ファイルがすごく大きいのでしょうか? nuko_suke_dev2025/01/22に更新あー、すみません、10分なのはCloud Buildのデフォルトの machineType で実行した結果です!ので実行する環境によってはもっと速くなるかと思います〜 ちなみに10分くらいかかってるファイルのサイズは大体100kB~200kBです。 なお、JSONで記述されている各オブジェクトは、オブジェクトや配列がネストされたような複雑なものでもなく、valueが文字列や数値の単純な形式のオブジェクトです。 簡易的ですがコード例も貼っておきます。 import fs from 'node:fs/promises'; import { compress } from 'compressed-json'; import JSONCrush from 'jsoncrush'; function heavyCompressJsonToString(value) { return JSONCrush.crush(JSON.stringify(compress(value))); } export async function writeDataToLocalFile(exportDir, data) { const t0 = performance.now(); await fs.writeFile(exportDir, heavyCompressJsonToString(data)); console.log(`Write ${performance.now() - t0} ms`); } 返信を追加
nuko_suke_dev2025/01/22に更新あー、すみません、10分なのはCloud Buildのデフォルトの machineType で実行した結果です!ので実行する環境によってはもっと速くなるかと思います〜 ちなみに10分くらいかかってるファイルのサイズは大体100kB~200kBです。 なお、JSONで記述されている各オブジェクトは、オブジェクトや配列がネストされたような複雑なものでもなく、valueが文字列や数値の単純な形式のオブジェクトです。 簡易的ですがコード例も貼っておきます。 import fs from 'node:fs/promises'; import { compress } from 'compressed-json'; import JSONCrush from 'jsoncrush'; function heavyCompressJsonToString(value) { return JSONCrush.crush(JSON.stringify(compress(value))); } export async function writeDataToLocalFile(exportDir, data) { const t0 = performance.now(); await fs.writeFile(exportDir, heavyCompressJsonToString(data)); console.log(`Write ${performance.now() - t0} ms`); }
Discussion
async functionやPromiseの中で実行をすればいい、という単純な話ではない感じですかね?
前提として
そのため、今回のような CPU バウンドな処理は Promise ベースの提供になっていないことが殆どで、そういった処理は明示的にプロセスやスレッドを起こさないと複数の CPU コアを利用した真の並列化は実現できません。
この記事ではスクレイピングの文脈とのことで、少なくともNode.jsではデフォルトで(必要に応じて)マルチスレッドが使用されるはずと思っています👀
なんとなくわかりやすそうなページ↓
マルチスレッドはOSによって複数のCPUコアが使われる場合がある認識ですが、ここが間違っているのでしょうか?
僕が引用した内容は「JSON圧縮が同期処理である」というもので、多重化・並行並列処理に関しては言及していないつもりです🙋♂️
またPromiseで行う処理が同期処理であるか非同期処理か、については非同期処理である、と考えているのですが、そこが間違っていますか?
話題に上げたらありがたいことにアドバイスをいただいたので、一応こちらにも上げておきます🙆♂️
もし興味がある方は、リプライツリーを見ていただけると。
少々語弊があったみたいですね,すみません。
こちらは語弊があったので無視してください。これが正しいかどうかは内包する処理に依存する,というのが正しい説明でした。
以下,仕切り直して説明します。具体例を挙げて,各ケースにおける「並行性」と「並列性」について説明します。
JSON.stringifyで大きなペイロードを文字列化することを想定してください。A.
JSON.stringifyをラップした JavaScript の同期関数コード例
説明
B.
JSON.stringifyをラップした JavaScript の Promise 関数コード例
説明
concurrentStringifyの実行に入ると全ての処理が終わるまでこの関数を抜けることはない。C.
JSON.stringifyをchild_process上で実行するようにラップした Node.js の Promise 関数コード例
stringify-worker.js:説明
child_processを使用することで別のプロセスで処理され,CPUのコアを活用した並列実行が可能D.
fs.readFileをラップした Node.js の Promise 関数コード例
説明
fs.readFileはlibuvのスレッドプールを利用するため,複数の IO 操作を並列で処理可能もとの話に戻りますが,A から B のように書き換えたところで, 「同期処理として提供されているものを
Promiseでラップしただけでは何も解決しないよ」 が私の一番いいたいところでした。Promiseでラップするだけで OK というのは, D のように元から並列性が実現されているものを取り扱う場合のみであり,そうなっていない処理の場合は基本的には C のような対応が必要ということです。child_processなど(他にも選択肢あり)を使用します。WebWorkerを使用します。単純に気になったのですが、10秒じゃなく10分ですか…?ファイルがすごく大きいのでしょうか?
あー、すみません、10分なのはCloud Buildのデフォルトの
machineTypeで実行した結果です!ので実行する環境によってはもっと速くなるかと思います〜ちなみに10分くらいかかってるファイルのサイズは大体100kB~200kBです。
なお、JSONで記述されている各オブジェクトは、オブジェクトや配列がネストされたような複雑なものでもなく、valueが文字列や数値の単純な形式のオブジェクトです。
簡易的ですがコード例も貼っておきます。