始めに
生のJavaScriptを書いていて、ここ1年でTypeScriptを利用し始めました。
型言語やオブジェクト指向についても改めて勉強して、実務でも扱うことに慣れてきた頃の話です。
ビッグデータをWeb上に表示するクローズドなサイトの新規機能開発でのことです。
ビッグデータの構成
具体的な内容は伏せるとして、月ごとの売上や顧客数などを元にしたデータです。こういったデータ構造を数百〜数千内包した配列を複数扱う必要がありました。
interface BigData {
date: string
key1: string
key2: string
value: number
}
const bigData: BigData[] = [
{
date: '2023-11-01',
key1: 'new',
key2: 'sales',
value: 100
}
]
日付の型を厳密に定義したい
文字列型の日付を扱っていると、引数を文字列で渡したり等価で評価したりする場面が多々あります。
const filterTargetDate = (bigData: BigData[], targetDate: string): BigData[] => {
return bigData.filter(data => data.date === targetDate)
}
日付が'YYYY-MM-DD'で固定されているのなら良いのですが、実際には複数のフォーマットが使われているのが現状です。
- YYYY-MM-DD
- YYYY/MM/DD
- YYYYMMDD
- YYYY-MM-DD HH:MM:SS
日付オブジェクトを使う
Dateやmomentのオブジェクトを利用すれば、日付を型として扱える + 等価評価が可能になります。
今回はPJで使っているmoment.jsを活用して、機能開発を進めました。小規模なテストデータの配列操作を行えるようにして、テストコードの方もクリアできました。
+ import moment, { Moment } from 'moment'
interface BigData {
- date: string
+ date: Moment
key1: string
key2: string
value: number
}
const bigData: BigData[] = [
{
- date: '2023-11-01',
+ date: moment('2023-11-01'),
key1: 'new',
key2: 'sales',
value: 100
}
]
- const filterTargetDate = (bigData: BigData[], targetDate: string): BigData[] => {
- return bigData.filter(data => data.date === targetDate)
+ const filterTargetDate = (bigData: BigData[], targetDate: Moment): BigData[] => {
+ return bigData.filter(data => targetDate.isSame(data.date))
}
moment.js は現在非推奨となっています。
もっさりとしたフロントの動作
テストを行ったのはあくまでも、ターミナル上での動作確認のみ。
Web上でユーザーが入力値を変えることで、ビッグデータ内の捜査(find, filter)と計算処理を行なって、リアルタイムに結果を表示する。これに約3秒程
かかっていたのです。
例とするならば、エクセルで万行単位のレコードがあるシートで、エクセル関数が複数カラム*列行で処理を行なっている状態で、対象のセルを変更した際にエクセル自体が停止してしまう。要は処理落ちということです。
PCとエクセル自体のメモリに限界があるのと同様に、ブラウザにも限界があって、処理に時間がかかってしまっているわけです。
フロントエンドにはVue.jsを利用して、不必要な再計算をさせないために、キャッシュ化(compute)もしています。
原因
コンピューティングのリソースが充実した最近ではあまり意識されないかもしれませんが、オブジェクト(クラス)の初期化はメモリを食います。
スマホアプリやコンシューマーゲーム、その他業務で利用する機械など、リソースが制限される環境ではメモリとパフォーマンスが重要視されるので、Web界隈とは勝手が異なります。
今回使ったmoment.js
も内部的にはDateオブジェクトを抱えていて、これを数千単位のデータ配列で扱っているのが原因でした。
ChromeのDevツールでパフォーマンスを計測したところ、moment.js
で同じ日付かをvalidするisSame()
の実行秒数がトータルでかなりかかっていることも確認しています。
解決案
日付文字列の代わりにDate系オブジェクト(moment.js
を含む)を使わずに、文字列を型として定義することにしました。
type oneToNine = 1|2|3|4|5|6|7|8|9
type zeroToNine = 0|1|2|3|4|5|6|7|8|9
type YYYY = `19${zeroToNine}${zeroToNine}` | `20${zeroToNine}${zeroToNine}`
type MM = `0${oneToNine}` | `1${0|1|2}`
type DD = `${0}${oneToNine}` | `${1|2}${zeroToNine}` | `3${0|1}`
export type HyphenYMD = `${YYYY}-${MM}-${DD}`
export type SlashYMD = `${YYYY}/${MM}/${DD}`
使用する時はformatをかけて返してあげるだけで、常に特定の日付文字列であることが静的に解析することが可能になります。
全くDate系オブジェクトを生成しないわけにはいきませんが、フォーマット処理自体は再計算時にされないため、実行秒数も約0.5秒程
に短縮できました。
import { format } from 'date-fns'
export const isInvalidDate = (date: Date) => Number.isNaN(date.getTime())
export const toHyphenYMD = (from: string | number | Date = new Date()): HyphenYMD => {
const date = new Date(from)
if (isInvalidDate(date)) throw new Error(`${from}はDate型で対応できません`)
const dateFormat = format(date, 'yyyy-MM-dd') as HyphenYMD
return dateFormat
}
export const toSlashYMD = (from: string | number | Date = new Date()): SlashYMD => {
const date = new Date(from)
if (isInvalidDate(date)) throw new Error(`${from}はDate型で対応できません`)
const dateFormat = format(date, 'yyyy/MM/dd') as SlashYMD
return dateFormat
}
終わりに
型を意識していない時であれば、日付文字をそのまま文字列として使っていたため、今回のような取り組みは行うことはなかったかと思います。結果的には想定していた工数よりも多くなって、業務が圧迫したわけですが…
ビッグデータではなく、単純な計算をするだけならDate系オブジェクトを使ってもパフォーマンスに影響はないです。
今回の件は珍しい例かもしれませんが、オブジェクトを大量に生成するのは良くないよねってことを、今後に活かしたいと思います。