34
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GASの処理速度が遅いのでNode.jsに移行したらwebアプリが高速に

Last updated at Posted at 2020-04-16

#はじめに
弓道で用いるwebアプリを開発しているんですが、データの読み書きがかなり遅かったので、
GoogleAppsScript+スプレッドシートからNode.js+Firebaseに移行しました。
(GoogleAppsScript Web APIからCloud Functions APIに変えたいうこと)

Google Apps ScriptもNode.jsも基本は一緒ですが...

これでかなりwebアプリの待機時間が短くなったので、記事にしようと思います。

私の使用用途は、スプレッドシートにユーザーやデータを保管し、それを読み書きする、つまりwebアプリのバックエンドとしての利用でGASの処理速度が遅いと感じたものであるので読む際は注意してください。

#結果から
どれだけ早くなったか先に紹介しておきます。

的中を記録し、その月のデータをとってくる処理を例に紹介します。記録するデータは日付、中たった数、引いた本数、的中率の四つのデータです。記録した後、付けた日の月のデータ全てとってきます。

↓Google Apps Script + スプレッドシート
gas.gif
ちなみにタイムは2.5秒ほど
スクリーンショット 2020-04-16 18.25.04.png

↓Node.js + Firebase(Cloud Functions API使用)

ezgif.com-video-to-gif (1).gif

スクリーンショット 2020-04-16 18.36.14.png

タイムは驚異の0.4秒!!!!!!!!!!!

約2秒ほど早くなりました。

*上のフェッチしてきたgifの後半に表示されるデータを見ればわかりますが、ばらつきがある状態で行ったので正確ではないですがほぼ同じデータ量です。

#私のサービスでGoogleAppsScriptを用いたスプレッドシートでの読み書きが遅いのはなぜか?

結論からいうと、部員の数だけAPIの呼び出しを行っていたため、スプレッドシートの読み書きが遅くなっていました。

Spreadsheetサービスでは、以下のようなメソッドなどを使ってスプレッドシートにアクセスをするたびにAPIが呼び出されます。

・getActiveSpreadsheet()
・getSheetByName()
・getDataRange()
・getLastRow()
・getRange()
・getValue()
・setValue()

二重のfor文の中で
sheet.getRange(i,j).getValue();
なんかした日には終わりで、行数×列数分APIアクセス実行されるので、かなり時間がかかります。

なので、このようにできるだけ呼び出すAPIを減らすために全てのセルのデータを配列として一回のアクセスでとるよう設計しています。
sheet.getDataRange().getValues();

しかし、私のwebアプリでは、部員全員のデータをとってくる処理があり、一回のAPIの呼び出し✖️部員数30で、計30回ほどAPIアクセスをする必要がありました。

この処理が遅い原因だったみたいです。

#GoogleAppsScript(WebAPI)の制約

GoogleAppsScript(WebAPI)の制約↓

https://www.bugbugnow.net/2018/12/GoogelAppsScript-restriction.html

一日のURLフェッチ(APIアクセス)が20,000/日、さらには無料のGoogleアカウントでは、スクリプトの実行時間が6分と地味に短いです

用途によっては十分な気もしますが、私の使用目的では、一日10回URLフェッチを行うとして、ユーザー数2000を超えると上限に達してしまいます。もう少し大規模なアプリにしようと思ったら物足りないですね。

有料プランもあるが、そちらも上限が決まっています。

#Cloud Functions APIの制約

それに比べてCloud Functions APIの制約というと

https://cloud.google.com/functions/pricing?hl=ja

呼び出し
関数の呼び出し料金は定額制です。HTTP リクエストから呼び出される関数(HTTP 関数)、バックグラウンド関数、call API から行われる呼び出しなど、呼び出し元によって料金が変わることはありません。

月間呼び出し回数 料金(100 万単位)
最初の 200 万回 無料
200 万回を超えた分 $0.40

米ドル以外の通貨でお支払いの場合は、Cloud Platform SKU に記載されている該当通貨の料金が適用されます。
呼び出し料金は 1 回あたり $0.0000004 の単価制で、関数の結果や実行時間に関係なく請求されます。ただし、毎月最初の 200 万回までは無料です。

無料枠がかなりあります。
一ユーザーが一日に10回APIアクセスするとして、1ヶ月で

10✖️30 = 300

毎月最初の 200 万回まで無料なので2000,000÷300 = 6,666人まで無料枠でいけます!

ユーザー数2,000を超えると上限に達してしまうGASのAPIに大してCloud Functions APIは 6,666人までいけるので、断然、Cloud Functions APIの方がいいですね。
**さらに超えた分に対しては超えた分の請求なので都合がいいです。**上限を超えた際には一回のAPIアクセスをするのに約0.0004円ほどで格安です。

ユーザーが6万6666人いるとして、毎月2,000万回APIアクセスするとしたら月約8,000円ほどなので、そこまで高くないですね。

#gas+スプレッドシート からNode.js + Firebaseに移行
以上よりGoogle Apps Script + スプレッドシート ⇨ Node.js + Firebase に移行しました。

gasのコード全部で500行くらいだったのでNode.jsに書き換えるのも一週間くらいで終わりました。

↓冒頭で見せたアプリ内で使われる的中率を書き込みに行き、月のデータをフェッチする処理
gasのコードとNode.jsのコードの比較(一部)を紹介します。

gas
//的中率書き込み
function submitFetchData(e){
  var sheetID =e.parameter["id"];
  var sheetName =e.parameter["name"];
  var date = e.parameter.p1;
  var hitArrow = e.parameter.p2;
  var allArrow = e.parameter.p3;
  var hitRate = hitArrow / allArrow * 100;
  var year =e.parameter["year"];
  var month = e.parameter["month"];
  //JSONオブジェクト格納用の入れ物
  var rowData = {}; 

  //書込先スプレッドシートのIDを入力
  var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName);
  var arrayData = [[ date, hitArrow, allArrow, hitRate]];

  var rows = arrayData.length;
  var cols = arrayData[0].length;

  sheet.insertRows(2,1);

  //シートに配列を書き込み
  sheet.getRange(2,1,1,cols).setValues(arrayData);
  
  var thisDate = year + month;
  var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName); 
  
  var data = sheet.getDataRange().getValues();
  var array = data.map(function(value,index){
    return (value.reduce(function(r,c,i){
      var result = r;
      switch(i){
        case 0: result.month = c;break;
      }
      return result;
    },{}))
  });
  array.shift();
  
  var date = array.map(function(value,index){
    var thisDate = {};
    var year,month;
    year = value.month.split('/');
    thisDate.year = year[0];
    month = year[1].split('/');
    thisDate.month = month[0];
    return thisDate;
  })
  var dateArray = date.map(function(value, index){
    return value.year + value.month;
  });
 
   var firstIndex = dateArray.indexOf(thisDate);
   var lastIndex = dateArray.lastIndexOf(thisDate);
      
  var newData = sheet.getRange(Number(firstIndex+2),1, Number(lastIndex+2) - Number(firstIndex+2) +1, data[0].length).getValues();  
  //オブジェクトに変換
  var newArray = newData.map(function(value,index){
    return (value.reduce(function(r,c,i){
      var result = r;
      switch(i){
        case 0: result.date = c;break;
        case 1: result.hit_arrow = c;break;
        case 2: result.all_arrow = c;break;
        case 3: result.hit_rate = c;break;
      }
      return result;
    },{}))
  }); 
   var output = ContentService.createTextOutput(JSON.stringify(newArray, null, 2));
   output.setMimeType(ContentService.MimeType.TEXT); 
   return output;
}

Node.js
//的中率書き込み&月のデータフェッチ
app.get('/hitWrite', async (req, res) => {
    console.log('-----hitWrite-----')
    const rec_name = req.query["rec_name"];
    const id = req.query["id"];
    const team = req.query["team"];
    const hit_arrow = req.query["hit_arrow"];
    const all_arrow = req.query["all_arrow"];
    const hit_rate = hit_arrow / all_arrow * 100;
    const date = req.query["date"];
    const year = req.query["year"];
    const month = req.query["month"];
    const arrayData = {
        all_arrow: Number(all_arrow),
        date,
        hit_arrow: Number(hit_arrow),
        hit_rate: Number(hit_rate),
        id: Math.random().toString(32).substring(2)//ランダムID生成
    };
    let records;
    await db.collection("college").doc(team).collection(id).doc("user_data").update({
        [rec_name]: admin.firestore.FieldValue.arrayUnion(arrayData)
    })
        .then(() => {
            return (
                db.collection("college").doc(team).collection(id).doc("user_data").get()
                    .then((snapshot) => {
                        return (
                            records = snapshot.data()[[rec_name]].filter((value) => {
                                if (value.date.split('/')[0] === year && value.date.split('/')[1] === month) {
                                    return value;
                                }
                            }),
                            res.json(JSON.stringify(records))
                        )
                    })
                    .catch((err) => {
                        console.log(err)
                        res.end();
                    })
            )
        })
        .catch((err) => {
            console.log(err)
            res.end();
        })

})

かなりすっきりしました。

読みやすい、書きやすい、共同開発しやすくなりました。

#おわりに

アプリの待機時間が短くなったので、Google Apps Script + スプレッドシート から Node.js + Firebase に移行してかなりよかったです。

ただ、金額的にも月にできるAPIアクセス数にも厳しさがあったGoogle Apps Scriptですが、スプレッドシートに書き込めれるので、部内でシートの共有なんかして直接記録を見たりできるのでこっちも便利だったりします。

ただ、ユーザー数が増えると厳しいのでやっぱり捨てざるをおえないってことでおさらばしないとけません。

このwebアプリのユーザー数は自分の大学のユーザーのみの30人なので、当分気にすることはないのですが...

最後まで見ていただき、ありがとうございました。

34
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?