Skip to content

Commit 52e11df

Browse files
committed
Implement faster icon loading by inlining icons from the Tabler sprite. The previous method required downloading and parsing a large file, causing delays in icon rendering. Now, icons are generated and cached, improving page load times. Update the icon helper to utilize the new inline method.
1 parent cec270e commit 52e11df

File tree

5 files changed

+92
-12
lines changed

5 files changed

+92
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- When response compression is enabled, additional buffering is needed. Users reported a better experience with pages that load more progressively, reducing the time before the pages' shell is rendered.
99
- When SQLPage is deployed behind a reverse proxy, compressing responses between sqlpage and the proxy is wasteful.
1010
- In the table component, allow simple objects in custom_actions instead of requiring arrays of objects.
11+
- Fatser icon loading. Previously, even a page containing a single icon required downloading and parsing a ~2MB file. This resulted in a delay where pages initially appeared with a blank space before icons appeared. Icons are now inlined inside pages and appear instantaneously.
1112

1213
## v0.39.0 (2025-10-28)
1314
- Ability to execute sql for URL paths with another extension. If you create sitemap.xml.sql, it will be executed for example.com/sitemap.xml

build.rs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ async fn main() {
2222
for h in [
2323
spawn(download_deps(c.clone(), "sqlpage.js")),
2424
spawn(download_deps(c.clone(), "sqlpage.css")),
25-
spawn(download_deps(c.clone(), "tabler-icons.svg")),
25+
spawn(download_tabler_icons(
26+
c.clone(),
27+
"https://cdn.jsdelivr.net/npm/@tabler/[email protected]/dist/tabler-sprite.svg",
28+
)),
2629
spawn(download_deps(c.clone(), "apexcharts.js")),
2730
spawn(download_deps(c.clone(), "tomselect.js")),
2831
spawn(download_deps(c.clone(), "favicon.svg")),
@@ -173,6 +176,73 @@ fn make_url_path(url: &str) -> PathBuf {
173176
sqlpage_artefacts.join(filename)
174177
}
175178

179+
async fn download_tabler_icons(client: Rc<awc::Client>, sprite_url: &str) {
180+
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
181+
let icon_map_path = out_dir.join("icons.rs");
182+
183+
if !icon_map_path.exists() {
184+
let cached_sprite_path = make_url_path(sprite_url);
185+
download_url_to_path(&client, sprite_url, &cached_sprite_path).await;
186+
generate_icons_rs(&icon_map_path, &cached_sprite_path);
187+
}
188+
}
189+
190+
fn generate_icons_rs(icon_map_path: &Path, cached_sprite_path: &Path) {
191+
let sprite_content = std::fs::read_to_string(cached_sprite_path).unwrap();
192+
let icons = extract_icons_from_sprite(&sprite_content);
193+
let mut file = File::create(icon_map_path).unwrap();
194+
195+
writeln!(file, "#[allow(clippy::all)]").unwrap();
196+
writeln!(file, "use std::collections::HashMap;").unwrap();
197+
writeln!(file, "use std::sync::LazyLock;").unwrap();
198+
writeln!(file).unwrap();
199+
writeln!(
200+
file,
201+
"pub static ICON_MAP: LazyLock<HashMap<String, &'static str>> = LazyLock::new(|| {{"
202+
)
203+
.unwrap();
204+
writeln!(file, " let mut m = HashMap::new();").unwrap();
205+
206+
for (name, content) in icons {
207+
writeln!(file, " m.insert({:?}.to_string(), r#\"{}\"#);", name, content).unwrap();
208+
}
209+
210+
writeln!(file, " m").unwrap();
211+
writeln!(file, "}});").unwrap();
212+
}
213+
214+
fn extract_icons_from_sprite(sprite_content: &str) -> Vec<(String, String)> {
215+
let mut icons = Vec::new();
216+
217+
let mut pos = 0;
218+
while let Some(symbol_start) = sprite_content[pos..].find("<symbol") {
219+
let symbol_start = pos + symbol_start;
220+
let Some(symbol_end) = sprite_content[symbol_start..].find("</symbol>") else {
221+
break;
222+
};
223+
let symbol_end = symbol_start + symbol_end + "</symbol>".len();
224+
225+
let symbol_tag = &sprite_content[symbol_start..symbol_end];
226+
227+
if let Some(id_start) = symbol_tag.find("id=\"tabler-") {
228+
let id_start = id_start + "id=\"tabler-".len();
229+
if let Some(id_end) = symbol_tag[id_start..].find('"') {
230+
let icon_name = &symbol_tag[id_start..id_start + id_end];
231+
232+
let content_start = symbol_tag.find('>').unwrap() + 1;
233+
let content_end = symbol_tag.rfind("</symbol>").unwrap();
234+
let inner_content = symbol_tag[content_start..content_end].trim();
235+
236+
icons.push((icon_name.to_string(), inner_content.to_string()));
237+
}
238+
}
239+
240+
pos = symbol_end;
241+
}
242+
243+
icons
244+
}
245+
176246
/// On debian-based linux distributions, odbc drivers are installed in /usr/lib/<target>-linux-gnu/odbc
177247
/// which is not in the default library search path.
178248
fn set_odbc_rpath() {

src/template_helpers.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ pub fn register_all_helpers(h: &mut Handlebars<'_>, config: &AppConfig) {
6767
register_helper(h, "app_config", AppConfigHelper(config.clone()));
6868

6969
// icon helper: generate an image with the specified icon
70-
h.register_helper("icon_img", Box::new(IconImgHelper(site_prefix)));
70+
h.register_helper("icon_img", Box::new(IconImgHelper));
7171
register_helper(h, "markdown", MarkdownHelper::new(config));
7272
register_helper(h, "buildinfo", buildinfo_helper as EH);
7373
register_helper(h, "typeof", typeof_helper as H);
@@ -209,8 +209,13 @@ impl CanHelp for AppConfigHelper {
209209
}
210210
}
211211

212-
/// Generate an image with the specified icon. Struct Param is the site prefix
213-
struct IconImgHelper(String);
212+
#[allow(clippy::unreadable_literal)]
213+
mod icons {
214+
include!(concat!(env!("OUT_DIR"), "/icons.rs"));
215+
}
216+
217+
/// Generate an image with the specified icon.
218+
struct IconImgHelper;
214219
impl HelperDef for IconImgHelper {
215220
fn call<'reg: 'rc, 'rc>(
216221
&self,
@@ -230,11 +235,15 @@ impl HelperDef for IconImgHelper {
230235
}
231236
};
232237
let size = params[1].as_u64().unwrap_or(24);
238+
239+
let Some(inner_content) = icons::ICON_MAP.get(name).copied() else {
240+
log::debug!("icon_img: icon {name} not found");
241+
return Ok(());
242+
};
243+
233244
write!(
234245
writer,
235-
"<svg width={size} height={size}><use href=\"{}{}#tabler-{name}\" /></svg>",
236-
self.0,
237-
static_filename!("tabler-icons.svg")
246+
"<svg viewBox=\"0 0 24 24\" width=\"{size}\" height=\"{size}\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">{inner_content}</svg>"
238247
)?;
239248
Ok(())
240249
}

src/webserver/http.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,6 @@ pub fn create_app(
437437
.service(static_content::apexcharts_js())
438438
.service(static_content::tomselect_js())
439439
.service(static_content::css())
440-
.service(static_content::icons())
441440
.service(static_content::favicon())
442441
.default_service(fn_service(main_handler)),
443442
)

src/webserver/static_content.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,27 @@ macro_rules! static_file_endpoint {
3030
}};
3131
}
3232

33+
#[must_use]
3334
pub fn js() -> Resource {
3435
static_file_endpoint!("sqlpage", "js", "application/javascript")
3536
}
3637

38+
#[must_use]
3739
pub fn apexcharts_js() -> Resource {
3840
static_file_endpoint!("apexcharts", "js", "application/javascript")
3941
}
4042

43+
#[must_use]
4144
pub fn tomselect_js() -> Resource {
4245
static_file_endpoint!("tomselect", "js", "application/javascript")
4346
}
4447

48+
#[must_use]
4549
pub fn css() -> Resource {
4650
static_file_endpoint!("sqlpage", "css", "text/css")
4751
}
4852

49-
pub fn icons() -> Resource {
50-
static_file_endpoint!("tabler-icons", "svg", "image/svg+xml")
51-
}
52-
53+
#[must_use]
5354
pub fn favicon() -> Resource {
5455
static_file_endpoint!("favicon", "svg", "image/svg+xml")
5556
}

0 commit comments

Comments
 (0)