Fase 4.1: Bootstrap Icons subset (94% reduccion)

Optimizacion PageSpeed:
- Original: 211 KB (2050 iconos)
- Subset: 13 KB (104 iconos usados)
- Ahorro: 198 KB (94% reduccion)

Cambios:
- Creado script create-icons-subset.py para generar subsets
- Generado bootstrap-icons-subset.min.css (4.5 KB)
- Generado bootstrap-icons-subset.woff2 (8.7 KB)
- Agregado font-display:swap (elimina bloqueo de 420ms)
- Actualizado enqueue-scripts.php para usar subset

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-27 14:34:52 -06:00
parent cd09666f1d
commit 2f19a7c077
6 changed files with 413 additions and 3 deletions

127
Assets/Vendor/bootstrap-icons-subset.css vendored Normal file
View File

@@ -0,0 +1,127 @@
/*!
* Bootstrap Icons Subset - ROI Theme
* 104 iconos de 2050 originales (5.1%)
* Generado automaticamente - NO EDITAR
*/
@font-face{
font-display:swap;
font-family:bootstrap-icons;
src:url("fonts/bootstrap-icons-subset.woff2") format("woff2"),
url("fonts/bootstrap-icons-subset.woff") format("woff")
}
.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{
display:inline-block;
font-family:bootstrap-icons!important;
font-style:normal;
font-weight:400!important;
font-variant:normal;
text-transform:none;
line-height:1;
vertical-align:-.125em;
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale
}
.bi-arrow-counterclockwise::before{content:"\f117"}
.bi-arrow-down-circle::before{content:"\f119"}
.bi-arrow-right::before{content:"\f138"}
.bi-arrow-up::before{content:"\f148"}
.bi-arrows-angle-expand::before{content:"\f14a"}
.bi-arrows-expand::before{content:"\f14c"}
.bi-arrows-fullscreen::before{content:"\f14d"}
.bi-arrows-move::before{content:"\f14e"}
.bi-arrows-vertical::before{content:"\f698"}
.bi-aspect-ratio::before{content:"\f150"}
.bi-badge-ad::before{content:"\f161"}
.bi-bar-chart::before{content:"\f17e"}
.bi-bootstrap::before{content:"\f1a8"}
.bi-bounding-box::before{content:"\f1b6"}
.bi-box-arrow-up-right::before{content:"\f1c5"}
.bi-c-circle::before{content:"\f7db"}
.bi-calendar-check::before{content:"\f1e2"}
.bi-calendar3::before{content:"\f214"}
.bi-card-image::before{content:"\f226"}
.bi-card-text::before{content:"\f228"}
.bi-chat-dots::before{content:"\f24a"}
.bi-chat-dots-fill::before{content:"\f249"}
.bi-chat-quote::before{content:"\f255"}
.bi-chat-text::before{content:"\f267"}
.bi-check-circle::before{content:"\f26b"}
.bi-chevron-down::before{content:"\f282"}
.bi-clock::before{content:"\f293"}
.bi-code::before{content:"\f2c8"}
.bi-code-slash::before{content:"\f2c6"}
.bi-cursor::before{content:"\f2e3"}
.bi-display::before{content:"\f302"}
.bi-envelope::before{content:"\f32f"}
.bi-envelope-fill::before{content:"\f32c"}
.bi-envelope-paper::before{content:"\f73d"}
.bi-exclamation-octagon::before{content:"\f337"}
.bi-exclamation-triangle::before{content:"\f33b"}
.bi-eye::before{content:"\f341"}
.bi-facebook::before{content:"\f344"}
.bi-file-earmark-text::before{content:"\f38b"}
.bi-file-text::before{content:"\f3b9"}
.bi-filetype-css::before{content:"\f742"}
.bi-filetype-js::before{content:"\f74c"}
.bi-folder-fill::before{content:"\f3d1"}
.bi-fonts::before{content:"\f3da"}
.bi-gear::before{content:"\f3e5"}
.bi-geo-alt-fill::before{content:"\f3e7"}
.bi-globe::before{content:"\f3ee"}
.bi-graph-up::before{content:"\f3f2"}
.bi-grid::before{content:"\f3fc"}
.bi-grid-3x3-gap::before{content:"\f3f9"}
.bi-hand-index::before{content:"\f403"}
.bi-hourglass::before{content:"\f421"}
.bi-hourglass-split::before{content:"\f41f"}
.bi-image::before{content:"\f42a"}
.bi-info-circle::before{content:"\f431"}
.bi-input-cursor::before{content:"\f436"}
.bi-input-cursor-text::before{content:"\f435"}
.bi-instagram::before{content:"\f437"}
.bi-key::before{content:"\f44f"}
.bi-layout-sidebar::before{content:"\f45f"}
.bi-layout-text-window-reverse::before{content:"\f463"}
.bi-lightning::before{content:"\f46f"}
.bi-lightning-charge-fill::before{content:"\f46c"}
.bi-link::before{content:"\f471"}
.bi-link-45deg::before{content:"\f470"}
.bi-linkedin::before{content:"\f472"}
.bi-list::before{content:"\f479"}
.bi-list-nested::before{content:"\f474"}
.bi-list-ol::before{content:"\f475"}
.bi-list-ul::before{content:"\f478"}
.bi-magic::before{content:"\f675"}
.bi-megaphone::before{content:"\f484"}
.bi-megaphone-fill::before{content:"\f483"}
.bi-menu-button-wide::before{content:"\f489"}
.bi-mouse::before{content:"\f499"}
.bi-palette::before{content:"\f4b1"}
.bi-person::before{content:"\f4e1"}
.bi-person-lines-fill::before{content:"\f4db"}
.bi-phone::before{content:"\f4e7"}
.bi-pin::before{content:"\f4ed"}
.bi-power::before{content:"\f4ff"}
.bi-send-fill::before{content:"\f6b9"}
.bi-share::before{content:"\f52e"}
.bi-shield-check::before{content:"\f52f"}
.bi-shield-lock::before{content:"\f538"}
.bi-shield-x::before{content:"\f53e"}
.bi-slash-circle::before{content:"\f567"}
.bi-sliders::before{content:"\f56b"}
.bi-square::before{content:"\f584"}
.bi-star::before{content:"\f588"}
.bi-star-fill::before{content:"\f586"}
.bi-stars::before{content:"\f589"}
.bi-tablet::before{content:"\f5ae"}
.bi-tag::before{content:"\f5b0"}
.bi-tags::before{content:"\f5b2"}
.bi-telephone-fill::before{content:"\f5b4"}
.bi-text-center::before{content:"\f5c4"}
.bi-text-paragraph::before{content:"\f5c8"}
.bi-three-dots::before{content:"\f5d4"}
.bi-toggle-on::before{content:"\f5d6"}
.bi-twitter-x::before{content:"\f8db"}
.bi-type::before{content:"\f5f7"}
.bi-whatsapp::before{content:"\f618"}
.bi-x-circle::before{content:"\f623"}

View File

@@ -0,0 +1 @@
@font-face{font-display:swap;font-family:bootstrap-icons;src:url("fonts/bootstrap-icons-subset.woff2") format("woff2"),url("fonts/bootstrap-icons-subset.woff") format("woff")}.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bi-arrow-counterclockwise::before{content:"\f117"}.bi-arrow-down-circle::before{content:"\f119"}.bi-arrow-right::before{content:"\f138"}.bi-arrow-up::before{content:"\f148"}.bi-arrows-angle-expand::before{content:"\f14a"}.bi-arrows-expand::before{content:"\f14c"}.bi-arrows-fullscreen::before{content:"\f14d"}.bi-arrows-move::before{content:"\f14e"}.bi-arrows-vertical::before{content:"\f698"}.bi-aspect-ratio::before{content:"\f150"}.bi-badge-ad::before{content:"\f161"}.bi-bar-chart::before{content:"\f17e"}.bi-bootstrap::before{content:"\f1a8"}.bi-bounding-box::before{content:"\f1b6"}.bi-box-arrow-up-right::before{content:"\f1c5"}.bi-c-circle::before{content:"\f7db"}.bi-calendar-check::before{content:"\f1e2"}.bi-calendar3::before{content:"\f214"}.bi-card-image::before{content:"\f226"}.bi-card-text::before{content:"\f228"}.bi-chat-dots::before{content:"\f24a"}.bi-chat-dots-fill::before{content:"\f249"}.bi-chat-quote::before{content:"\f255"}.bi-chat-text::before{content:"\f267"}.bi-check-circle::before{content:"\f26b"}.bi-chevron-down::before{content:"\f282"}.bi-clock::before{content:"\f293"}.bi-code::before{content:"\f2c8"}.bi-code-slash::before{content:"\f2c6"}.bi-cursor::before{content:"\f2e3"}.bi-display::before{content:"\f302"}.bi-envelope::before{content:"\f32f"}.bi-envelope-fill::before{content:"\f32c"}.bi-envelope-paper::before{content:"\f73d"}.bi-exclamation-octagon::before{content:"\f337"}.bi-exclamation-triangle::before{content:"\f33b"}.bi-eye::before{content:"\f341"}.bi-facebook::before{content:"\f344"}.bi-file-earmark-text::before{content:"\f38b"}.bi-file-text::before{content:"\f3b9"}.bi-filetype-css::before{content:"\f742"}.bi-filetype-js::before{content:"\f74c"}.bi-folder-fill::before{content:"\f3d1"}.bi-fonts::before{content:"\f3da"}.bi-gear::before{content:"\f3e5"}.bi-geo-alt-fill::before{content:"\f3e7"}.bi-globe::before{content:"\f3ee"}.bi-graph-up::before{content:"\f3f2"}.bi-grid::before{content:"\f3fc"}.bi-grid-3x3-gap::before{content:"\f3f9"}.bi-hand-index::before{content:"\f403"}.bi-hourglass::before{content:"\f421"}.bi-hourglass-split::before{content:"\f41f"}.bi-image::before{content:"\f42a"}.bi-info-circle::before{content:"\f431"}.bi-input-cursor::before{content:"\f436"}.bi-input-cursor-text::before{content:"\f435"}.bi-instagram::before{content:"\f437"}.bi-key::before{content:"\f44f"}.bi-layout-sidebar::before{content:"\f45f"}.bi-layout-text-window-reverse::before{content:"\f463"}.bi-lightning::before{content:"\f46f"}.bi-lightning-charge-fill::before{content:"\f46c"}.bi-link::before{content:"\f471"}.bi-link-45deg::before{content:"\f470"}.bi-linkedin::before{content:"\f472"}.bi-list::before{content:"\f479"}.bi-list-nested::before{content:"\f474"}.bi-list-ol::before{content:"\f475"}.bi-list-ul::before{content:"\f478"}.bi-magic::before{content:"\f675"}.bi-megaphone::before{content:"\f484"}.bi-megaphone-fill::before{content:"\f483"}.bi-menu-button-wide::before{content:"\f489"}.bi-mouse::before{content:"\f499"}.bi-palette::before{content:"\f4b1"}.bi-person::before{content:"\f4e1"}.bi-person-lines-fill::before{content:"\f4db"}.bi-phone::before{content:"\f4e7"}.bi-pin::before{content:"\f4ed"}.bi-power::before{content:"\f4ff"}.bi-send-fill::before{content:"\f6b9"}.bi-share::before{content:"\f52e"}.bi-shield-check::before{content:"\f52f"}.bi-shield-lock::before{content:"\f538"}.bi-shield-x::before{content:"\f53e"}.bi-slash-circle::before{content:"\f567"}.bi-sliders::before{content:"\f56b"}.bi-square::before{content:"\f584"}.bi-star::before{content:"\f588"}.bi-star-fill::before{content:"\f586"}.bi-stars::before{content:"\f589"}.bi-tablet::before{content:"\f5ae"}.bi-tag::before{content:"\f5b0"}.bi-tags::before{content:"\f5b2"}.bi-telephone-fill::before{content:"\f5b4"}.bi-text-center::before{content:"\f5c4"}.bi-text-paragraph::before{content:"\f5c8"}.bi-three-dots::before{content:"\f5d4"}.bi-toggle-on::before{content:"\f5d6"}.bi-twitter-x::before{content:"\f8db"}.bi-type::before{content:"\f5f7"}.bi-whatsapp::before{content:"\f618"}.bi-x-circle::before{content:"\f623"}

281
Assets/Vendor/create-icons-subset.py vendored Normal file
View File

@@ -0,0 +1,281 @@
#!/usr/bin/env python3
"""
Bootstrap Icons Subset Generator
Genera CSS y fuente optimizados solo con los iconos usados en roi-theme
"""
import re
import subprocess
import os
# Directorio base
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# Iconos usados en roi-theme (extraidos con grep)
USED_ICONS = [
"bi-arrow-counterclockwise",
"bi-arrow-down-circle",
"bi-arrow-right",
"bi-arrows-angle-expand",
"bi-arrows-expand",
"bi-arrows-fullscreen",
"bi-arrows-move",
"bi-arrows-vertical",
"bi-arrow-up",
"bi-aspect-ratio",
"bi-badge-ad",
"bi-bar-chart",
"bi-bootstrap",
"bi-bounding-box",
"bi-box-arrow-up-right",
"bi-calendar3",
"bi-calendar-check",
"bi-card-image",
"bi-card-text",
"bi-c-circle",
"bi-chat-dots",
"bi-chat-dots-fill",
"bi-chat-quote",
"bi-chat-text",
"bi-check-circle",
"bi-chevron-down",
"bi-clock",
"bi-code",
"bi-code-slash",
"bi-cursor",
"bi-display",
"bi-envelope",
"bi-envelope-fill",
"bi-envelope-paper",
"bi-exclamation-octagon",
"bi-exclamation-triangle",
"bi-eye",
"bi-facebook",
"bi-file-earmark-text",
"bi-file-text",
"bi-filetype-css",
"bi-filetype-js",
"bi-folder-fill",
"bi-fonts",
"bi-gear",
"bi-geo-alt-fill",
"bi-globe",
"bi-graph-up",
"bi-grid",
"bi-grid-3x3-gap",
"bi-hand-index",
"bi-hourglass",
"bi-hourglass-split",
"bi-image",
"bi-info-circle",
"bi-input-cursor",
"bi-input-cursor-text",
"bi-instagram",
"bi-key",
"bi-layout-sidebar",
"bi-layout-text-window-reverse",
"bi-lightning",
"bi-lightning-charge-fill",
"bi-link",
"bi-link-45deg",
"bi-linkedin",
"bi-list",
"bi-list-nested",
"bi-list-ol",
"bi-list-ul",
"bi-magic",
"bi-megaphone",
"bi-megaphone-fill",
"bi-menu-button-wide",
"bi-mouse",
"bi-palette",
"bi-person",
"bi-person-lines-fill",
"bi-phone",
"bi-pin",
"bi-power",
"bi-send-fill",
"bi-share",
"bi-shield-check",
"bi-shield-lock",
"bi-shield-x",
"bi-slash-circle",
"bi-sliders",
"bi-square",
"bi-star",
"bi-star-fill",
"bi-stars",
"bi-tablet",
"bi-tag",
"bi-tags",
"bi-telephone-fill",
"bi-text-center",
"bi-text-paragraph",
"bi-three-dots",
"bi-toggle-on",
"bi-twitter-x",
"bi-type",
"bi-whatsapp",
"bi-x-circle",
]
def read_original_css():
"""Lee el CSS original de Bootstrap Icons"""
css_path = os.path.join(BASE_DIR, "bootstrap-icons.min.css")
with open(css_path, "r", encoding="utf-8") as f:
return f.read()
def extract_unicode_codes(css_content, icons):
"""Extrae los codigos unicode de los iconos usados"""
codes = {}
for icon in icons:
# Buscar .bi-nombre::before{content:"\XXXX"}
pattern = rf'\.{re.escape(icon)}::before\{{content:"\\([a-f0-9]+)"\}}'
match = re.search(pattern, css_content, re.IGNORECASE)
if match:
codes[icon] = match.group(1)
else:
print(f"WARNING: No se encontro codigo para {icon}")
return codes
def generate_subset_css(codes):
"""Genera el CSS subset con solo los iconos usados"""
css_lines = []
# Header
css_lines.append("/*!")
css_lines.append(" * Bootstrap Icons Subset - ROI Theme")
css_lines.append(f" * {len(codes)} iconos de 2050 originales ({len(codes)/2050*100:.1f}%)")
css_lines.append(" * Generado automaticamente - NO EDITAR")
css_lines.append(" */")
# @font-face con font-display: swap
css_lines.append('@font-face{')
css_lines.append(' font-display:swap;')
css_lines.append(' font-family:bootstrap-icons;')
css_lines.append(' src:url("fonts/bootstrap-icons-subset.woff2") format("woff2"),')
css_lines.append(' url("fonts/bootstrap-icons-subset.woff") format("woff")')
css_lines.append('}')
# Estilos base para .bi
css_lines.append('.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{')
css_lines.append(' display:inline-block;')
css_lines.append(' font-family:bootstrap-icons!important;')
css_lines.append(' font-style:normal;')
css_lines.append(' font-weight:400!important;')
css_lines.append(' font-variant:normal;')
css_lines.append(' text-transform:none;')
css_lines.append(' line-height:1;')
css_lines.append(' vertical-align:-.125em;')
css_lines.append(' -webkit-font-smoothing:antialiased;')
css_lines.append(' -moz-osx-font-smoothing:grayscale')
css_lines.append('}')
# Reglas para cada icono
for icon, code in sorted(codes.items()):
css_lines.append(f'.{icon}::before{{content:"\\{code}"}}')
return '\n'.join(css_lines)
def generate_minified_css(codes):
"""Genera CSS minificado"""
parts = []
# @font-face
parts.append('@font-face{font-display:swap;font-family:bootstrap-icons;src:url("fonts/bootstrap-icons-subset.woff2") format("woff2"),url("fonts/bootstrap-icons-subset.woff") format("woff")}')
# Estilos base
parts.append('.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}')
# Iconos
icon_rules = [f'.{icon}::before{{content:"\\{code}"}}' for icon, code in sorted(codes.items())]
parts.append(''.join(icon_rules))
return ''.join(parts)
def create_font_subset(codes):
"""Crea subset de la fuente usando pyftsubset"""
# Crear archivo con unicodes
unicodes = ','.join([f'U+{code}' for code in codes.values()])
# Paths
woff2_src = os.path.join(BASE_DIR, "fonts", "bootstrap-icons.woff2")
woff2_dst = os.path.join(BASE_DIR, "fonts", "bootstrap-icons-subset.woff2")
woff_src = os.path.join(BASE_DIR, "fonts", "bootstrap-icons.woff")
woff_dst = os.path.join(BASE_DIR, "fonts", "bootstrap-icons-subset.woff")
# Crear subset woff2
print(f"Creando subset woff2 con {len(codes)} glyphs...")
cmd_woff2 = [
"pyftsubset",
woff2_src,
f"--unicodes={unicodes}",
f"--output-file={woff2_dst}",
"--flavor=woff2"
]
subprocess.run(cmd_woff2, check=True)
# Crear subset woff
print(f"Creando subset woff con {len(codes)} glyphs...")
cmd_woff = [
"pyftsubset",
woff_src,
f"--unicodes={unicodes}",
f"--output-file={woff_dst}",
"--flavor=woff"
]
subprocess.run(cmd_woff, check=True)
# Mostrar tamanos
orig_size = os.path.getsize(woff2_src) / 1024
new_size = os.path.getsize(woff2_dst) / 1024
print(f"woff2: {orig_size:.1f} KB -> {new_size:.1f} KB ({(1-new_size/orig_size)*100:.1f}% reduccion)")
def main():
print("=== Bootstrap Icons Subset Generator ===\n")
# 1. Leer CSS original
print("1. Leyendo CSS original...")
css_content = read_original_css()
# 2. Extraer codigos unicode
print(f"2. Extrayendo codigos de {len(USED_ICONS)} iconos...")
codes = extract_unicode_codes(css_content, USED_ICONS)
print(f" Encontrados: {len(codes)} iconos")
# 3. Generar CSS subset
print("3. Generando CSS subset...")
subset_css = generate_subset_css(codes)
css_path = os.path.join(BASE_DIR, "bootstrap-icons-subset.css")
with open(css_path, "w", encoding="utf-8") as f:
f.write(subset_css)
print(f" Guardado: bootstrap-icons-subset.css")
# 4. Generar CSS minificado
print("4. Generando CSS minificado...")
min_css = generate_minified_css(codes)
min_path = os.path.join(BASE_DIR, "bootstrap-icons-subset.min.css")
with open(min_path, "w", encoding="utf-8") as f:
f.write(min_css)
orig_css_size = os.path.getsize(os.path.join(BASE_DIR, "bootstrap-icons.min.css")) / 1024
new_css_size = os.path.getsize(min_path) / 1024
print(f" CSS: {orig_css_size:.1f} KB -> {new_css_size:.1f} KB ({(1-new_css_size/orig_css_size)*100:.1f}% reduccion)")
# 5. Crear font subset
print("5. Creando font subset...")
try:
create_font_subset(codes)
except Exception as e:
print(f" ERROR: {e}")
print(" Intenta ejecutar: pip install fonttools brotli")
return
print("\n=== COMPLETADO ===")
print("Archivos generados:")
print(" - bootstrap-icons-subset.css (legible)")
print(" - bootstrap-icons-subset.min.css (minificado)")
print(" - fonts/bootstrap-icons-subset.woff2")
print(" - fonts/bootstrap-icons-subset.woff")
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.