Skip to content

Instantly share code, notes, and snippets.

@tgrushka
Last active January 26, 2025 23:16
Show Gist options
  • Save tgrushka/ecfe2ec78ae2d5859887431021e4673d to your computer and use it in GitHub Desktop.
Save tgrushka/ecfe2ec78ae2d5859887431021e4673d to your computer and use it in GitHub Desktop.
Flutter Web Loading Progress Bar
// prettier-ignore
{{flutter_js}}
// prettier-ignore
{{flutter_build_config}}
// Place this file at web/flutter_bootstrap.js in your Flutter Web project.
// IMPORTANT! The two tokens at the top of this file must not contain any spaces.
const APP_NAME = document.title
document.body.style.backgroundColor = "#000"
const SPEED_3G_BPS = 1500000 // 1.5 Mbps for 3G
const SPEED_HOME_BPS = SPEED_3G_BPS * 10
// If > 0, will throttle fetch to simulate slow network speeds.
const SIMULATE_SPEED = 0
// Flutter Web Initialization Documentation:
// https://docs.flutter.dev/platform-integration/web/initialization#example-display-a-progress-indicator
const container = document.createElement("div")
container.style.cssText = `
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
gap: 2rem;
overflow: hidden;
`
const progressDescription = document.createElement("div")
progressDescription.style.cssText = `
color: #fff;
text-align: center;
align-self: center;
font-family: Arial, Helvetica, sans-serif;
font-size: 2em;
white-space: nowrap;
`
progressDescription.textContent = `Loading ${
typeof APP_NAME !== "undefined" ? APP_NAME : "application"
}...`
const progressContainer = document.createElement("div")
progressContainer.style.cssText = `
margin: 0 20%;
`
const progressRow = document.createElement("div")
progressRow.style.cssText = `
display: flex;
align-items: center;
gap: 1em;
`
const progressBg = document.createElement("div")
progressBg.style.cssText = `
flex-grow: 1;
padding: 5px;
border-radius: 16px;
height: 1em;
background: #333;
`
const progress = document.createElement("div")
progress.style.cssText = `
width: 0%;
height: 100%;
border-radius: 9px;
background: #0175C2;
transition: width 0.3s;
`
const percentage = document.createElement("div")
percentage.style.cssText = `
color: #fff;
font-family: Arial, Helvetica, sans-serif;
font-size: 1em;
white-space: nowrap;
`
percentage.textContent = "0%"
const timeRemaining = document.createElement("div")
timeRemaining.style.cssText = `
color: #fff;
text-align: center;
margin-top: 1em;
font-family: Arial, Helvetica, sans-serif;
font-size: 1em;
`
timeRemaining.textContent = "Estimating time remaining..."
progressBg.appendChild(progress)
progressRow.appendChild(progressBg)
progressRow.appendChild(percentage)
progressContainer.appendChild(progressRow)
progressContainer.appendChild(timeRemaining)
container.appendChild(progressDescription)
container.appendChild(progressContainer)
document.body.appendChild(container)
const originalFetch = window.fetch
const stats = new Map()
const startTime = Date.now()
window.fetch = async (...args) => {
// Get content length from HEAD request first
const [url] = args
const headResponse = await originalFetch.apply(window, [
url,
{ method: "HEAD" },
])
if (headResponse.status >= 400) {
return await originalFetch.apply(window, args)
}
// Content-Encoding header not available in browser, so estimate compression ratio:
const ratio = 3.5
const contentLengthHeader = headResponse.headers.get("Content-Length")
if (contentLengthHeader) {
const contentLength = parseInt(contentLengthHeader)
if (contentLength > 0) {
const total = contentLength * ratio
stats.set(url, {
total,
loaded: 0,
})
} else {
console.error("Content-Length = 0 or NaN for", url)
}
} else {
console.error("Content-Length header not found for", url)
}
// Proceed with original fetch
const response = await originalFetch.apply(window, args)
const reader = response.body.getReader()
return new Response(
new ReadableStream({
async start(controller) {
while (true) {
const { done, value } = await reader.read()
if (
!done &&
typeof SIMULATE_SPEED === "number" &&
SIMULATE_SPEED > 0
) {
await new Promise((resolve) =>
setTimeout(
resolve,
((value.length * 8) / SIMULATE_SPEED) * 1000
)
)
}
if (stats.has(url)) {
const stat = stats.get(url)
if (done) {
// Update total for this URL from estimated to actual downloaded bytes.
stat.total = stat.loaded
} else {
stat.loaded += value.length
}
const totalBytes = Array.from(stats.values()).reduce(
(sum, s) => sum + s.total,
0
)
const loadedBytes = Array.from(stats.values()).reduce(
(sum, s) => sum + s.loaded,
0
)
const percent = Math.floor(
(loadedBytes / totalBytes) * 100
)
progress.style.width = `${percent}%`
percentage.textContent = `${percent}%`
// Calculate time remaining
const elapsed = (Date.now() - startTime) / 1000
const rate = loadedBytes / elapsed // bytes per second
const remaining = Math.ceil(
(totalBytes - loadedBytes) / rate
)
timeRemaining.textContent =
remaining > 0
? `about ${remaining} seconds remaining`
: ""
}
if (done) {
break
}
controller.enqueue(value)
}
controller.close()
},
}),
response
)
}
_flutter.loader.load({
onEntrypointLoaded: async function (engineInitializer) {
// This is the long step that downloads the WASM binary.
const appRunner = await engineInitializer.initializeEngine()
progressDescription.textContent = "Running app..."
// Restore original fetch so it doesn't interfere with your app.
window.fetch = originalFetch
await appRunner.runApp()
},
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment