前言
最美好的使用體驗是即刻的,因此追求更短暫的等待時間越快越早的回饋很重要,然而倉促的將任何當下的資訊拋給用戶未必是最佳體驗,舉例來說:
- 進入網頁
- 反饋「載入資料中……」
- 顯示載入完成的資料
以上流程簡潔實在,會有什麼問題呢?問題出在時機和時間。
不要呈現尷尬的體驗給用戶
用戶沒辦法在 30 毫秒內了解載入發生後結束,只會看到一團資訊閃過
追求盡可能即時的回饋是好事,但如果載入資訊因為「反饋即時」而很快過期,只會變成一團無法理解的雜訊體驗,思考:
- 載入體驗的形式
- 用戶需要花多少時間理解
- 載入體驗存在的必要與意義
用戶對感知的經驗法則
像是 0.1 / 1 / 10 秒的概念來自 Response Times: The 3 Important Limits分割出了三種體驗的定義:
- 0.1 秒:即時感
- 1 秒:流暢感
- 10 秒:耐心極限
雖然不同族群、時代、個體和載入體驗對於都有不同的標準和影響,但還是可以有相關的概念。
不同形式的載入體驗
為了避免用戶等待造成不好的體驗,界面建構適當的反饋是必要的,具體來說常見的載入界面有以下幾種,它們有各自的適用場景和取捨:
- 載入文字 Text Loader:一串文字用於告知載入狀態
- 載入圖示 Loader / Spinner:一個動畫圖示用於告知載入中的狀態
- 進度條 Progress Bar:一個進度條用於告知載入的進度
- UI 骨架 UI Skeleton:一個低仿真的閃爍界面告知載入中的狀態
像是 UI 骨架就能很好的緩和載入和成果的邊界,且有極低的認知負擔,甚至不太需要額外的動畫過渡緩和體驗,但並不一定適用任何界面。
最低載入時間代碼實踐
舉例三個顯示水果清單的元件,個別有 10 / 100 / 1000 毫秒的回應時間:
<FruitList :response-time="10"></FruitList><FruitList :response-time="100"></FruitList><FruitList :response-time="1000"></FruitList>
<template> <div class="fruit-list"> <h2>水果清單</h2>
<div class="actions"> <button @click="fetchFruits" :disabled="loading"> {{ loading ? '載入水果清單中…' : '取得水果清單' }} </button> <button @click="clear" :disabled="loading"> 清除水果清單 </button> </div>
<div class="status"> <p v-if="loading">等待回應...</p> </div>
<ul v-if="fruits.length && !loading"> <li v-for="(fruit, index) in fruits" :key="index">{{ fruit }}</li> </ul>
<p v-else-if="!loading && !fruits.length">目前沒有水果資料。</p> </div></template>
<script setup lang="ts">import { ref, onMounted } from 'vue'
const { responseTime } = defineProps<{ responseTime: number}>()
type Fruit = string
function getFruits(): Promise<Fruit[]> { return new Promise((resolve, reject) => { setTimeout(() => { resolve(['蘋果', '香蕉', '芒果', '奇異果']) }, responseTime) })}
const fruits = ref<Fruit[]>([])const loading = ref(false)
async function fetchFruits() { loading.value = true try { const result = await getFruits() fruits.value = result } catch (err: unknown) { fruits.value = [] } finally { loading.value = false }}
function clear() { fruits.value = []}
onMounted(() => { fetchFruits()})</script>
很明顯 10 毫秒的載入時間對大多數人來說理解都極為吃力,透過製作 promiseWithMinimumTime
函式來在合理的時機返回結果。
- 輸入:觸發函式與最短 n 毫秒觸發時間
- 計算:
- 大於 n 就執行
- 小於 n 就於 n 執行
<FruitList :response-time="10" :min-loading-time="100" /><FruitList :response-time="100" :min-loading-time="100" /><FruitList :response-time="1000" :min-loading-time="100" />
const { loading: isLoading, result: fruits, load: loadFruitList } = useLoadWithMinimumTime(getFruits, { minimumTime: minLoadingTime,})
import { ref, type Ref } from 'vue'
function promiseWithMinimumTime<T>(promise: Promise<T>, minimumTime: number): Promise<T> { return new Promise((resolve, reject) => { const startTime = Date.now()
promise .then(result => { const elapsedTime = Date.now() - startTime const remainingTime = minimumTime - elapsedTime
if (remainingTime > 0) { setTimeout(() => { resolve(result) }, remainingTime) } else { resolve(result) } }) .catch(error => { const elapsedTime = Date.now() - startTime const remainingTime = minimumTime - elapsedTime
if (remainingTime > 0) { setTimeout(() => { reject(error) }, remainingTime) } else { reject(error) } }) })}
interface UseLoadWithMinimumTimeOptions { minimumTime?: number}
export function useLoadWithMinimumTime<T>( promiseFn: () => Promise<T>, options: UseLoadWithMinimumTimeOptions = {},) { const { minimumTime = 0 } = options
const loading = ref(false) const error = ref('') const result = ref<T>()
async function load() { loading.value = true error.value = ''
try { const promise = promiseFn() const promiseWithMinTime = promiseWithMinimumTime(promise, minimumTime) const promiseResult = await promiseWithMinTime result.value = promiseResult } catch (err: unknown) { result.value = undefined if (err instanceof Error) { error.value = err.message } else if (typeof err === 'string') { error.value = err } else { error.value = 'An unknown error occurred.' } } finally { loading.value = false } }
return { loading, error, result, load, }}
總結
每個 UI 改變狀態時都應該恰當適當的時間甚至是動畫輔助理解。
如果用戶對於操作界面不熟悉或是有空閒的時間思考體驗上的問題可以考慮:
- 大多數使用者已經習慣相關 UI 體驗的問題
- 額外的邏輯要理解與維護
- 沒有太多實質性的效益,更多是對使用體驗的洞察與細微改善
延伸閱讀
- You Don’t Need Animations - Emil Kowalski
- Should a “loading” text or spinner stay a minimum time on screen?