大規模なデータをJSON.stringifyとBlobを使って保存しようとしたら,ブラウザがエラーを出すときの対処法

# 背景
JavaScriptでcsvを読み込んだ後、データを加工して、加工後のデータをダウンロードしようとしたたとき、Chromeのデバッガーにこんなメッセージが出てページの再読み込みを強制させられました。

>
Debugging connection was closed. Reason: Render process gone
Reconnect when ready by reopening DevTools


ダウンロードするデータのサイズが小さいときはうまくいきますが、
データのサイズが大きいときは工夫が必要なようです。
下記コードで800MBのデータをダウンロード保存しようとするとダメでした。

```javascript
const string = JSON.stringify(data)
const blob = new Blob([string],{type:{"text/plain"}})
```



# 問題
多次元配列でdataが用意されている。このdataをJSONとしてファイルに保存ようとすると、エラーが出て保存できない。ページの再読み込みを強制される。

dataの例： 1KBの文字列を要素として持つ100 x 100 x 100の3次元の配列data(1GB)

```javascript
const string = [...Array(10)].reduce((pre)=>pre+pre,"x");
console.log(`string size: ${string.length/1000} KB`) // string size: 1.024 KB

const N = 100
const data = [...Array(N)].map(k=>[...Array(N)].map(k=>[...Array(N)].map(k=>string)))
console.log(`data size: ${JSON.stringify(data).length/10**9} GB`) // data size: 1.027 GB
```

# 原因
原因は２つのようです。
(1) 文字列の長さの上限
JavaScriptでは文字列の長さに上限があります。ブラウザやブラウザが動いている環境によって違いますが、1GBあたりが限界みたいです。JSON.stringifyはJavaScriptのオブジェクトをJSONとして文字列化するので、JavaScriptで扱える文字列数を超えるとエラーが出ます。この場合コンソールに次のように出ます。

>
Uncaugtht RangeError: Invalid string length
 

(2)Blobの配列の要素の値のサイズの上限
Blobにもサイズの限界があり、下記コードのstringに入れられる文字数は500MB程度のようです。

```javascript
const blob = new Blob([string],{type:{"text/plain"}})
```

今回こちらで引っかかり、冒頭のエラーが出て、デバッガーツールが停止しました。




# 対処法
dataを小分けにしてJSON化して、Blobを作る時に入れる配列も小分けにすると問題を緩和できます。new Blob([string1,string2,string3,...)作戦です。

対処前

```javascript
const string = JSON.stringify(data)
const blob = new Blob([string],{type:{"text/plain"}})
```

対処後

```javascript
const stringList = data.map(JSON.stringify) 
const json = ['[']
stringList.forEach(string=>{
  list.push(string)
  list.push(',')
})
list.pop()
list.push(']')
const blob =new Blob([...json],{type:'text/plain'})
```

しかし、ファイルが1GBを超えるほど巨大化すると１つのファイルでは保存できなくなるので、ファイルを小分けにするとさらにその問題を回避できます。データが大きすぎるときはひとつのファイルにするのを諦める作戦です。

改良後

```javascript
const saveAs = (blob,filename)=>{
  const download = document.createElement("a")
  download.download = filename
  download.href = window.URL.createObjectURL(blob)
  download.click()
}
const stringList = data.map(JSON.stringify) 
const size = stringList.reduce((p,c)=>p+c.length, 0)
if(size<10**9){
  const json = ['[']
  stringList.forEach(string=>{
    list.push(string)
    list.push(',')
  })
  list.pop()
  list.push(']')
  const blob =new Blob([...json],{type:'text/plain'})
  const filename = 'sample.txt'
  saveAs(blob, filename)
}
else{
  stringList.forEach((json,index)=>{
    const blob =new Blob([json],{type:'text/plain'})
    const filename = 'sample_' + index + '.txt'
    saveAs(blob, filename)
  })
}
```


# 参考
(参考1)
下記コードでエラーが出なければ、文字列1GB, Blobの配列の要素の値のサイズ500MBは大丈夫です。

```javascript
const stringList = [...Array(30)].map((k,i)=>[...Array(29-i)].reduce(pre=>pre+pre,"x"));

const string = stringList.slice(0,25).reduce((pre,current)=>pre+current,"") 
console.log(`max string size: ${string.length/10**9} GB`)

const string2 = stringList.slice(0,1).reduce((pre,current)=>pre+current,"") 
const blob = new Blob([string2],{type:"text/plain"})
console.log(`max blob size: ${blob.size/10**9} GB`)
```

(参考2) ChromeのBlobの上限
https://chromium.googlesource.com/chromium/src/+/master/storage/browser/blob/README.md

(参考3)
ChromeではJavaScriptのメモリの使用量と使用限界を確認できます。

```javascript
console.log(window.performance.memory)
```
https://webplatform.github.io/docs/apis/timing/properties/memory/

# [jsnote](https://qiita.com/Toyoharu-Nishikawa/items/044d53974c42bc635f0f)の改良

上記問題に対処するため、1GBを超えるデータをダウンロード保存できるようにjsnoteを改良しました。
[ソースコード](https://github.com/Toyoharu-Nishikawa/jsnote)


[![Imgur](https://i.imgur.com/HstpPha.png)](https://qiita.com/Toyoharu-Nishikawa/items/044d53974c42bc635f0f) 

改良後にjsnoteで試したコード

```javascript:test.js
const string = [...Array(10)].reduce((pre)=>pre+pre,"x");
console.log(`string size: ${string.length/1000} KB`) // string size: 1.024 KB

const N = 100
const data = [...Array(N)].map(k=>[...Array(N)].map(k=>[...Array(N)].map(k=>string)))
console.log(`data size: ${JSON.stringify(data).length/10**9} GB`) // data size: 1.027 GB

exportFileName ="sample.txt"
exportText = data.map(JSON.stringify)

console.log("ready to export")
```
(注意)上記コードをjsnoteに張り付けて実行し、exportボタンを押すと10MB×100ファイル=1GBのデータ(中身は"x"で埋め尽くされているだけ)を保存するので、注意してください。