package stringutil import ( "regexp" "strings" ) // imgAttrPrefix skips over complete attributes so the name can't match inside another // attribute's name (data-loading) or a quoted value (alt="loading=x"). const imgAttrPrefix = `(?is)^]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]*))?)*\s+` var ( imgTagRe = regexp.MustCompile(`(?is)"'])*>`) imgLoadingAttrRe = regexp.MustCompile(imgAttrPrefix + `loading\s*=`) imgDecodingAttrRe = regexp.MustCompile(imgAttrPrefix + `decoding\s*=`) ) // DeferOffscreenImages adds loading="lazy" and decoding="async" to every tag // except the first, which is left eager since it is the likely LCP element. func DeferOffscreenImages(html string) string { n := 0 return imgTagRe.ReplaceAllStringFunc(html, func(tag string) string { n++ if n == 1 { return tag } var attrs strings.Builder if !imgLoadingAttrRe.MatchString(tag) { attrs.WriteString(` loading="lazy"`) } if !imgDecodingAttrRe.MatchString(tag) { attrs.WriteString(` decoding="async"`) } if attrs.Len() == 0 { return tag } closing := ">" body := strings.TrimSuffix(tag, ">") if trimmed, ok := strings.CutSuffix(body, "/"); ok { body = trimmed closing = " />" } return strings.TrimRight(body, " \t\r\n") + attrs.String() + closing }) }