From d60000148455b6be2af9604d6740efce08490ea2 Mon Sep 17 00:00:00 2001 From: AJADI ABDULMALIK <68543198+Malik6828@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:42:10 +0000 Subject: [PATCH] feat(perf): implement advanced performance optimization with code splitting (#450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vite.config.js: add build.target=es2020, cssCodeSplit, esbuild minify, assetsInlineLimit=4096, drop debugger in prod, resolve extensions for .js→.ts compat, zustand+react-router in react-vendor chunk - src/hooks/usePreload.ts: new preloadTab() / usePreload hook that triggers dynamic imports on nav hover (prefetch-on-hover) - src/components/layout/Sidebar.tsx: call preloadTab(id) on onMouseEnter for every nav button so chunks are fetched before the user clicks - .github/workflows/ci.yml: replace single 500 KB main-chunk check with per-chunk budgets (main 50 KB, vendor 500 KB, stellar-sdk 800 KB) and a 2 MB total-JS gzip budget; report all chunks in step summary - Fix pre-existing broken imports that blocked the build: - UserPreferences: AccessibilitySettings wrong relative path - offline.js: ./logger.js → ./logger (resolves to .ts) - stateSync.js: add missing syncState / onStateChange exports - stellar.ts: add fetchAccountDetails / AccountDetails / isPublicKey aliases - payments.ts: new shim re-exporting fetchPaymentPaths as fetchPathPayments - WalletConnect: fix Card named→default import --- .github/workflows/ci.yml | 84 ++++++++++++------- src/components/dashboard/WalletConnect.tsx | 2 +- src/components/layout/Sidebar.tsx | 2 + .../preferences/UserPreferences.jsx | 2 +- src/hooks/usePreload.ts | 61 ++++++++++++++ src/lib/payments.ts | 4 + src/lib/stellar.ts | 8 ++ src/utils/offline.js | 2 +- src/utils/stateSync.js | 33 +++++++- vite.config.js | 34 ++++++-- 10 files changed, 191 insertions(+), 41 deletions(-) create mode 100644 src/hooks/usePreload.ts create mode 100644 src/lib/payments.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a89eac2..c035562a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -207,7 +207,7 @@ jobs: retention-days: 7 # ── Bundle size budget ──────────────────────────────────────────────────── - # Fails the PR if the gzipped main entry chunk exceeds BUDGET_KB. + # Fails the PR if any chunk or total bundle exceeds its gzip budget. # Add the label "skip-bundle-check" to the PR to bypass (e.g. intentional # large dependency bump that will be addressed in a follow-up). bundle-size: @@ -215,8 +215,11 @@ jobs: runs-on: ubuntu-latest needs: [lint, type-check, test] env: - # Gzip budget for the main entry chunk in kilobytes. - BUDGET_KB: 500 + # Per-chunk gzip budgets in kilobytes. + BUDGET_MAIN_KB: 50 # app entry chunk (index-*.js) + BUDGET_VENDOR_KB: 500 # any single vendor chunk + BUDGET_STELLAR_KB: 800 # stellar-sdk chunk (large, but isolated) + BUDGET_TOTAL_KB: 2000 # total gzipped JS across all chunks steps: - uses: actions/checkout@v6 @@ -230,48 +233,69 @@ jobs: - name: Build production bundle run: npm run build - - name: Measure gzipped bundle size + - name: Measure gzipped bundle sizes id: measure run: | - # Find the main entry JS chunk (assets/index-*.js or assets/main-*.js) - MAIN_CHUNK=$(ls dist/assets/index-*.js dist/assets/main-*.js 2>/dev/null | head -n 1) - if [ -z "$MAIN_CHUNK" ]; then - echo "::error::Could not locate main entry chunk in dist/assets/" - exit 1 - fi - GZIP_BYTES=$(gzip -c "$MAIN_CHUNK" | wc -c) - GZIP_KB=$(( GZIP_BYTES / 1024 )) - echo "main_chunk=$MAIN_CHUNK" >> "$GITHUB_OUTPUT" - echo "gzip_kb=$GZIP_KB" >> "$GITHUB_OUTPUT" - echo "budget_kb=$BUDGET_KB" >> "$GITHUB_OUTPUT" + set -euo pipefail + echo "### Bundle Size Report" >> "$GITHUB_STEP_SUMMARY" - echo "| Metric | Value |" >> "$GITHUB_STEP_SUMMARY" - echo "|--------|-------|" >> "$GITHUB_STEP_SUMMARY" - echo "| Main chunk | \`$MAIN_CHUNK\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Gzipped size | **${GZIP_KB} KB** |" >> "$GITHUB_STEP_SUMMARY" - echo "| Budget | ${BUDGET_KB} KB |" >> "$GITHUB_STEP_SUMMARY" - if [ "$GZIP_KB" -gt "$BUDGET_KB" ]; then - echo "| Status | ❌ Over budget by $(( GZIP_KB - BUDGET_KB )) KB |" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Chunk | Gzip (KB) | Budget (KB) | Status |" >> "$GITHUB_STEP_SUMMARY" + echo "|-------|-----------|-------------|--------|" >> "$GITHUB_STEP_SUMMARY" + + FAILED=0 + TOTAL_KB=0 + + for CHUNK in dist/assets/*.js; do + [ -f "$CHUNK" ] || continue + CHUNK_NAME=$(basename "$CHUNK") + GZIP_KB=$(( $(gzip -c "$CHUNK" | wc -c) / 1024 )) + TOTAL_KB=$(( TOTAL_KB + GZIP_KB )) + + # Determine per-chunk budget + if echo "$CHUNK_NAME" | grep -qE '^(index|main)-'; then + BUDGET=$BUDGET_MAIN_KB + elif echo "$CHUNK_NAME" | grep -q 'stellar-sdk'; then + BUDGET=$BUDGET_STELLAR_KB + else + BUDGET=$BUDGET_VENDOR_KB + fi + + if [ "$GZIP_KB" -gt "$BUDGET" ]; then + echo "| \`$CHUNK_NAME\` | **${GZIP_KB}** | ${BUDGET} | ❌ over by $(( GZIP_KB - BUDGET )) KB |" >> "$GITHUB_STEP_SUMMARY" + echo "::warning::$CHUNK_NAME is ${GZIP_KB} KB gzipped (budget: ${BUDGET} KB)." + FAILED=$(( FAILED + 1 )) + else + echo "| \`$CHUNK_NAME\` | ${GZIP_KB} | ${BUDGET} | ✅ |" >> "$GITHUB_STEP_SUMMARY" + fi + done + + echo "" >> "$GITHUB_STEP_SUMMARY" + if [ "$TOTAL_KB" -gt "$BUDGET_TOTAL_KB" ]; then + echo "**Total JS (gzip): ${TOTAL_KB} KB — ❌ exceeds total budget of ${BUDGET_TOTAL_KB} KB**" >> "$GITHUB_STEP_SUMMARY" + echo "::warning::Total JS is ${TOTAL_KB} KB gzipped (budget: ${BUDGET_TOTAL_KB} KB)." + FAILED=$(( FAILED + 1 )) else - echo "| Status | ✅ Within budget ($(( BUDGET_KB - GZIP_KB )) KB headroom) |" >> "$GITHUB_STEP_SUMMARY" + echo "**Total JS (gzip): ${TOTAL_KB} KB — ✅ within total budget of ${BUDGET_TOTAL_KB} KB**" >> "$GITHUB_STEP_SUMMARY" fi - - name: Enforce budget (skippable via label) - # Skip enforcement when the PR carries the override label. + echo "failed=$FAILED" >> "$GITHUB_OUTPUT" + echo "total_kb=$TOTAL_KB" >> "$GITHUB_OUTPUT" + + - name: Enforce budgets (skippable via label) if: > !contains( github.event.pull_request.labels.*.name, 'skip-bundle-check' ) run: | - GZIP_KB=${{ steps.measure.outputs.gzip_kb }} - BUDGET_KB=${{ steps.measure.outputs.budget_kb }} - if [ "$GZIP_KB" -gt "$BUDGET_KB" ]; then - echo "::error::Main bundle is ${GZIP_KB} KB gzipped — exceeds the ${BUDGET_KB} KB budget." + FAILED=${{ steps.measure.outputs.failed }} + if [ "$FAILED" -gt "0" ]; then + echo "::error::$FAILED chunk(s) exceeded their size budget. See the step summary for details." echo "::error::To bypass, add the 'skip-bundle-check' label to this PR and document the reason." exit 1 fi - echo "✅ Main bundle is ${GZIP_KB} KB gzipped (budget: ${BUDGET_KB} KB)." + echo "✅ All chunks within budget. Total JS: ${{ steps.measure.outputs.total_kb }} KB gzipped." - name: Upload dist artifact uses: actions/upload-artifact@v7 diff --git a/src/components/dashboard/WalletConnect.tsx b/src/components/dashboard/WalletConnect.tsx index cb2167b5..866dd01e 100644 --- a/src/components/dashboard/WalletConnect.tsx +++ b/src/components/dashboard/WalletConnect.tsx @@ -19,7 +19,7 @@ import { readSecurityAuditLog, getSessionSecurityPosture, } from '../../lib/wallet/security' -import { Card } from './Card' +import Card from './Card' interface WalletDef { id: string diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 1a892bb3..56c08e58 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -4,6 +4,7 @@ import { useStore } from '../../lib/store'; import CopyableValue from '../dashboard/CopyableValue'; import { NETWORKS, updateCustomNetworkConfig, switchToCustomProfile, loadCustomNetworkProfiles } from '../../lib/stellar'; import { getActiveProfile } from '../../lib/userPreferences'; +import { preloadTab } from '../../hooks/usePreload'; const SESSION_API_KEY = 'stellar_custom_api_key'; @@ -409,6 +410,7 @@ export default function Sidebar({ isMobile = false }: SidebarProps) { e.currentTarget.style.background = 'var(--bg-hover)' e.currentTarget.style.color = 'var(--text-primary)' } + if (item.id) preloadTab(item.id) }} onMouseLeave={e => { if (!isActive && !isDisabled) { diff --git a/src/components/preferences/UserPreferences.jsx b/src/components/preferences/UserPreferences.jsx index 6295453d..f7b430ac 100644 --- a/src/components/preferences/UserPreferences.jsx +++ b/src/components/preferences/UserPreferences.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { usePreferences } from '../../hooks/usePreferences' import AddressBook from './AddressBook' import ThemeSettings from './ThemeSettings' -import AccessibilitySettings from './AccessibilitySettings' +import AccessibilitySettings from '../accessibility/AccessibilitySettings' import { showTestNotification } from '../../utils/offline' import { Bell } from 'lucide-react' diff --git a/src/hooks/usePreload.ts b/src/hooks/usePreload.ts new file mode 100644 index 00000000..5aab8d08 --- /dev/null +++ b/src/hooks/usePreload.ts @@ -0,0 +1,61 @@ +/** + * usePreload — prefetch a lazy-loaded tab chunk when the user hovers over its nav link. + * + * Usage: + * const { preload } = usePreload() + *