iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
Modern Web

教練看不到的那六天|從 FIT 檔到 3D 軌跡,馬拉松訓練資料 Dashboard系列 第 29

Day 29|拆 bundle 上線:把 Three.js 從首屏拿掉之後

  • 分享至 

  • xImage
  •  

到昨天為止,教練其實看不到儀表板。

它一直跑在本地 localhost:5173,上課現場我拿給教練看的是手機裡的 HTML 檔,不是網址。

dist/ 丟上去這件事本身十分鐘就做完了,部落格就掛在 Cloudflare,加個子網域的事。

npm run build 按下去,第一件撞到的不是部署,是 build 工具自己跳出來的警告。


一包 707 KB

dist/index.html                   0.49 kB │ gzip:   0.36 kB
dist/assets/index-BQh4EfYZ.css   22.19 kB │ gzip:   5.19 kB
dist/assets/index-CXWknSPw.js   707.00 kB │ gzip: 197.25 kB

(!) Some chunks are larger than 500 kB after minification.

整個儀表板打包成一個 JS 檔,707 KB。

裡面有五個分頁:總覽、六場全馬、配速計算、3D 路線、體重機。

而其中一個分頁跟其他四個完全不是同一個量級:3D 路線那頁要 Three.js。 node_modules/three 本體 25 MB,打包壓完之後也還有 580 KB,佔了整包的八成。

問題在於:打開這個儀表板的人,第一眼看到的是總覽頁。

但瀏覽器在畫出總覽頁之前,已經把整個 3D 引擎載下來了。 不管那個人會不會點進 3D 那頁。

以教練來說,他大概不會,他要看的是課表達成率和這週練了什麼,不必看東京馬的軌跡在空中轉。


拆法:讓分頁自己去載自己的東西

Vue 有一個現成的做法:defineAsyncComponent 搭配動態 import()

原本 App.vue 頂端是五行普通的 import,五個分頁的程式碼在 build 時被打成同一包。改成這樣:

import { ref, defineAsyncComponent } from 'vue';
import OverviewView from './views/OverviewView.vue';

// 總覽是進站第一眼,同步載入。其餘分頁動態載入 ——
// 3D 路線那頁光是 three.js 就佔掉整包的一半以上,而多數人不會點進去。
// 打開總覽卻先下載整個 3D 引擎,是沒有道理的。
const RacesView = defineAsyncComponent(() => import('./views/RacesView.vue'));
const Route3DView = defineAsyncComponent(() => import('./views/Route3DView.vue'));
const ScaleView = defineAsyncComponent(() => import('./views/ScaleView.vue'));
const PaceView = defineAsyncComponent(() => import('./views/PaceView.vue'));

只有總覽維持同步載入,因為那是每個人進站第一眼一定會看到的東西,把它也拆出去只會多一次來回。

其他四個分頁改成「點到才載」。import() 寫成函式呼叫,build 工具看到這個語法就知道:這個檔案要切成獨立的一塊,等真的被呼叫再抓。

<template> 那邊一個字都不用改,v-if 切分頁的寫法照舊。


拆完之後

dist/index.html                        0.49 kB │ gzip:   0.36 kB
dist/assets/index-BQh4EfYZ.css        22.19 kB │ gzip:   5.19 kB
dist/assets/races-DQPEsZcJ.js          1.65 kB │ gzip:   1.08 kB
dist/assets/ScaleView-jpjt604J.js      4.90 kB │ gzip:   2.99 kB
dist/assets/PaceView-BoN0vEuf.js       7.06 kB │ gzip:   3.10 kB
dist/assets/RacesView-BalnynDg.js     19.33 kB │ gzip:   7.64 kB
dist/assets/index-DmpXxFDx.js         96.81 kB │ gzip:  38.66 kB
dist/assets/Route3DView-j789yytu.js  580.98 kB │ gzip: 148.05 kB

一包變八個檔。

拆分前 拆分後
首屏 JS 707.00 kB 96.81 kB −86%
首屏 JS(gzip) 197.25 kB 38.66 kB −80%

進站只載 97 KB:Vue 本體、Pinia、總覽頁,Three.js 那 580 KB 變成 Route3DView 自己的一塊,點進 3D 分頁的那一刻才開始下載。

其他三個分頁也各自成一塊,但它們都很小,拆不拆對首屏影響不到 30 KB。真正有差的只有 3D 那一塊。


那個警告沒有消失

再看一次拆分後的 build 輸出,最後一行:

(!) Some chunks are larger than 500 kB after minification.

還在

它抱怨的對象從 index 變成 Route3DView,580 KB 還是超過 500 KB 的門檻。警告沒有被解掉,只是從「每個人都要付」搬到「點進 3D 的人才要付」。

要不要繼續拆?

Three.js 本體就是那麼大,它是一個完整的 3D 引擎,可以再往下切,例如把 OrbitControls 之類的附加模組再分一塊,但那頁一進去就會用到那些東西,分開只是多幾次 request,總量沒有變少。

所以這裡的判斷是:這個警告是一個門檻,不是一個錯誤。 它提醒的是「有人會為這一塊付出下載成本」,而現在付這個成本的只剩真的要看 3D 的人,這個代價我覺得合理。


上線前最後一道關:資料要不要跟著上去

bundle 拆完,剩下的是資料。

這個 repo 是公開的,而 public/ 底下有三個檔案被 .gitignore 擋在外面:

public/sessions.json    逐日訓練紀錄
public/weights.json     體重與阻抗
public/load.json        ATL / CTL / TSB

擋掉的理由 Day 18 提過:訓練紀錄會露出作息和常去的地點,體重是健康資料,程式碼公開沒問題,但資料不必跟著。

那公開版能放什麼?

六場全馬的路線本來就是公開的 ,東京馬、福岡馬、臺北馬的路線任何人都查得到,我的 GPS 軌跡跟官方路線圖差不了多少,3D 路線用的東京馬軌跡也是,這些可以上。

日常訓練的 GPS 不行 ,每一筆都會有訓練地點,容易透露習慣的訓練時間跟地點。

所以公開版和本機版跑的是同一套 code,差別只在 public/ 裡放了哪些檔案:

分頁 本機 公開版
總覽(達成率、體重、負荷) 沒有資料
六場全馬
配速計算
3D 路線 ✓(東京馬、8/13 間歇)
體重機 需要 Web Bluetooth,公開版無意義

這裡有一個容易漏掉的地方:.gitignore 擋的是 repo,不是 deploy。

那三個檔案在本機的 public/ 底下好好地躺著,npm run build 會原封不動把它們複製進 dist/。而 Cloudflare 的 wrangler deploy 上傳的就是整個 dist/。也就是說,什麼都不做直接部署,體重和逐日訓練紀錄會跟著上線,.gitignore 從頭到尾沒有機會攔。

所以多了一個 build:public,build 完先把不該上去的東西清掉再上傳:

"build:public": "npm run build && rm -f dist/sessions.json dist/weights.json dist/load.json && find dist/races -name '*.json' -not -name '*.min.json' -delete",
"deploy": "npm run build:public && wrangler deploy"

順手把那 3.5 MB 從沒被讀過的完整逐秒檔也清掉了,公開版的 dist/ 剩 2.5 MB。

那總覽頁在公開版長什麼樣?三個 JSON 都是 404,原本會在訓練紀錄卡片裡顯示一行「讀不到訓練紀錄:HTTP 404」,像壞掉。現在改成:三個檔都 404 就當作是公開版,頂端放一張說明卡,本週跑量和負荷曲線顯示「僅本機」,不放示意數字。

https://ithelp.ithome.com.tw/upload/images/20260914/20183598amqQKUZQVD.jpg

一套 code、兩份資料。公開版掛在部落格的子網域底下,跟主站互不干擾:

https://training.superbeeee.com

上線後直接打三個個人資料的網址確認過,sessions.jsonweights.jsonload.json 都是 404,完整逐秒的賽事檔也是,不該上傳的東西都不在。


所以第①項解了嗎

誠實地說:解了一半。

教練現在有一個網址可以開了:training.superbeeee.com。他點進去看得到六場全馬疊在一起、看得到東京馬的軌跡在空中轉、可以算配速。

但他真正要看的是總覽那頁:這週練了什麼、達成率多少、負荷曲線長什麼樣,那頁在公開版上是空的。 那些資料不能上網,所以教練能看到「我練得如何」的地方,仍是 Day 18 那份單檔 HTML 週報。

第一項的「教練看不到儀表板」,變成了「教練看得到儀表板,但看不到裡面最重要的那頁」。

不是沒有辦法,Cloudflare Access 設一條 policy、或自己寫十幾行 Worker 擋 Basic Auth,都能讓教練登入後看到總覽頁。真正讓我停手的是它改變了保證的性質——現在的保證是「那些檔案不在網路上」,加了鎖之後變成「那些檔案在網路上,但有一道鎖」。「不在」確認一次就結束了,它不會自己變回來;「在、但被擋住」得一直確認下去,而且我只驗得到我想得到的那幾個網址。

這是資料隱私和「給教練看」之間的取捨。三十天內我沒把握維護後者,所以留著週報那條路,待未來再改善。


明天:回到第一天的問題

系統的部分到今天為止告一個段落。

Day 1 的時候我寫過:最後一天,這套系統要能回答那個問題:我練得如何。

明天不寫新功能,明天打開它,問它這個問題。



上一篇
Day 28|3D 路線的垂直軸:不一定要放海拔
下一篇
Day 30|訓練 Dashboard 我練得如何:打開它,問它第一天的那個問題
系列文
教練看不到的那六天|從 FIT 檔到 3D 軌跡,馬拉松訓練資料 Dashboard30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言