// Put header content to dummy css position
if (false !== preg_match(self::DUMMY_CSS_REGEX, $this->content)) {
self::debug('Put optm data to dummy css location');
$this->content = preg_replace( self::DUMMY_CSS_REGEX, $this->html_head, $this->content );
// Fallback: try to be after charset
elseif (strpos($this->content, '<meta charset') !== false) {
self::debug('Put optm data to be after <meta charset>');
$this->content = preg_replace('#<meta charset([^>]*)>#isU', '<meta charset$1>' . $this->html_head, $this->content, 1);
self::debug('Put optm data to be after <head>');
$this->content = preg_replace('#<head([^>]*)>#isU', '<head$1>' . $this->html_head, $this->content, 1);
// Replace html foot part
$this->html_foot = apply_filters('litespeed_optm_html_foot', $this->html_foot);
$this->content = str_replace('</body>', $this->html_foot . '</body>', $this->content);
// Drop noscript if enabled
if ($this->conf(self::O_OPTM_NOSCRIPT_RM)) {
// $this->content = preg_replace( '#<noscript>.*</noscript>#isU', '', $this->content );
// Inline font-face optimize
$this->content = $this->__optimizer->optm_font_face( $this->content );
if (defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_HTML_MIN)) {
$this->content = $this->__optimizer->html_min($this->content);
private function _build_js_tag( $src ) {
if ($this->cfg_js_defer === 2 || Utility::str_hit_array($src, $this->cfg_js_delay_inc)) {
return '<script data-optimized="1" type="litespeed/javascript" data-src="' . Str::trim_quotes($src) . '"></script>';
if ($this->cfg_js_defer) {
return '<script data-optimized="1" src="' . Str::trim_quotes($src) . '" defer></script>';
return '<script data-optimized="1" src="' . Str::trim_quotes($src) . '"></script>';
* Build a full inline JS snippet
private function _build_js_inline( $script, $minified = false ) {
if ($this->cfg_js_defer) {
$deferred = $this->_js_inline_defer($script, false, $minified);
return '<script>' . $script . '</script>';
private function _maybe_js_delay() {
if ($this->cfg_js_defer !== 2 && !$this->cfg_js_delay_inc) {
if (!defined('LITESPEED_JS_DELAY_LIB_LOADED')) {
define('LITESPEED_JS_DELAY_LIB_LOADED', true);
$this->html_foot .= '<script>' . File::read(LSCWP_DIR . self::LIB_FILE_JS_DELAY) . '</script>';
private function _async_ggfonts() {
if (!$this->cfg_ggfonts_async || !$this->_ggfonts_urls) {
self::debug2('google fonts async found: ', $this->_ggfonts_urls);
$this->html_head_early .= '<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin />';
* Could be multiple fonts
* <link rel='stylesheet' href='//fonts.googleapis.com/css?family=Open+Sans%3A400%2C600%2C700%2C800%2C300&ver=4.9.8' type='text/css' media='all' />
* <link rel='stylesheet' href='//fonts.googleapis.com/css?family=PT+Sans%3A400%2C700%7CPT+Sans+Narrow%3A400%7CMontserrat%3A600&subset=latin&ver=4.9.8' type='text/css' media='all' />
* -> family: PT Sans:400,700|PT Sans Narrow:400|Montserrat:600
* <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,300italic,400italic,600,700,900&subset=latin%2Clatin-ext' />
$script = 'WebFontConfig={google:{families:[';
foreach ($this->_ggfonts_urls as $v) {
$qs = wp_specialchars_decode($v);
$qs = parse_url($qs, PHP_URL_QUERY);
if (empty($qs['family'])) {
self::debug('ERR ggfonts failed to find family: ' . $v);
$subset = empty($qs['subset']) ? '' : ':' . $qs['subset'];
foreach (array_filter(explode('|', $qs['family'])) as $v2) {
$families[] = Str::trim_quotes($v2 . $subset);
$script .= '"' . implode('","', $families) . ($this->_conf_css_font_display ? '&display=swap' : '') . '"';
// if webfontloader lib was loaded before WebFontConfig variable, call WebFont.load
$script .= 'if ( typeof WebFont === "object" && typeof WebFont.load === "function" ) { WebFont.load( WebFontConfig ); }';
$html = $this->_build_js_inline($script);
// https://cdnjs.cloudflare.com/ajax/libs/webfont/1.6.28/webfontloader.js
$webfont_lib_url = LSWCP_PLUGIN_URL . self::LIB_FILE_WEBFONTLOADER;
// default async, if js defer set use defer
$html .= $this->_build_js_tag($webfont_lib_url);
// Put this in the very beginning for preconnect
$this->html_head = $html . $this->html_head;
private function _font_optm() {
if (!$this->_conf_css_font_display || !$this->_ggfonts_urls) {
self::debug2('google fonts optm ', $this->_ggfonts_urls);
foreach ($this->_ggfonts_urls as $v) {
if (strpos($v, 'display=')) {
$this->html_head = str_replace($v, $v . '&display=swap', $this->html_head);
$this->html_foot = str_replace($v, $v . '&display=swap', $this->html_foot);
$this->content = str_replace($v, $v . '&display=swap', $this->content);
* @since 1.7.1 DNS prefetch
* @since 5.6.1 DNS preconnect
private function _dns_optm_init() {
// Widely enable link DNS prefetch
if (defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_DNS_PREFETCH_CTRL)) {
@header('X-DNS-Prefetch-Control: on');
$this->dns_prefetch = $this->conf(self::O_OPTM_DNS_PREFETCH);
$this->dns_preconnect = $this->conf(self::O_OPTM_DNS_PRECONNECT);
if (!$this->dns_prefetch && !$this->dns_preconnect) {
if (function_exists('wp_resource_hints')) {
add_filter('wp_resource_hints', array( $this, 'dns_optm_filter' ), 10, 2);
add_action('litespeed_optm', array( $this, 'dns_optm_output' ));
public function dns_optm_filter( $urls, $relation_type ) {
if ('dns-prefetch' === $relation_type) {
foreach ($this->dns_prefetch as $v) {
if ('preconnect' === $relation_type) {
foreach ($this->dns_preconnect as $v) {
* DNS optm output directly
* @since 1.7.1 DNS prefetch
* @since 5.6.1 DNS preconnect
public function dns_optm_output() {
foreach ($this->dns_prefetch as $v) {
$this->html_head_early .= '<link rel="dns-prefetch" href="' . Str::trim_quotes($v) . '" />';
foreach ($this->dns_preconnect as $v) {
$this->html_head_early .= '<link rel="preconnect" href="' . Str::trim_quotes($v) . '" crossorigin />';
* Run minify with src queue list
private function _src_queue_handler( $src_list, $html_list, $file_type = 'css' ) {
$html_list_ori = $html_list;
$can_webp = $this->cls('Media')->webp_support();
$tag = $file_type == 'css' ? 'link' : 'script';
foreach ($src_list as $key => $src_info) {
if (!empty($src_info['inl'])) {
if ($file_type == 'css') {
$code = Optimizer::minify_css($src_info['src']);
$can_webp && ($code = $this->cls('Media')->replace_background_webp($code));
$snippet = str_replace($src_info['src'], $code, $html_list[$key]);
if ($this->cfg_js_defer) {
$attrs = !empty($src_info['attrs']) ? $src_info['attrs'] : '';
$snippet = $this->_js_inline_defer($src_info['src'], $attrs) ?: $html_list[$key];
$code = Optimizer::minify_js($src_info['src']);
$snippet = str_replace($src_info['src'], $code, $html_list[$key]);
$url = $this->_build_single_hash_url($src_info['src'], $file_type);
$snippet = str_replace($src_info['src'], $url, $html_list[$key]);
if ($file_type == 'css' && $this->cfg_css_async) {
$snippet = $this->_async_css($snippet);
if ($file_type === 'js' && $this->cfg_js_defer) {
$snippet = $this->_js_defer($snippet, $src_info['src']) ?: $snippet;
$snippet = str_replace("<$tag ", '<' . $tag . ' data-optimized="1" ', $snippet);
$html_list[$key] = $snippet;
$this->content = str_replace($html_list_ori, $html_list, $this->content);
* Build a single URL mapped filename (This will not save in DB)
private function _build_single_hash_url( $src, $file_type = 'css' ) {
$content = $this->__optimizer->load_file($src, $file_type);
$is_min = $this->__optimizer->is_min($src);
$content = $this->__optimizer->optm_snippet($content, $file_type, !$is_min, $src);
$filepath_prefix = $this->_build_filepath_prefix($file_type);
$filename = $filepath_prefix . md5($this->remove_query_strings($src)) . '.' . $file_type;
$static_file = LITESPEED_STATIC_DIR . $filename;
File::save($static_file, $content, true);
// QS is required as $src may contains version info
$qs_hash = substr(md5($src), -5);
return LITESPEED_STATIC_URL . "$filename?ver=$qs_hash";
* Generate full URL path with hash for a list of src
private function _build_hash_url( $src_list, $file_type = 'css' ) {
// $url_sensitive = $this->conf( self::O_OPTM_CSS_UNIQUE ) && $file_type == 'css'; // If need to keep unique CSS per URI
// Replace preserved ESI (before generating hash)
if ($file_type == 'js') {
foreach ($src_list as $k => $v) {
$src_list[$k]['src'] = $this->_preserve_esi($v['src']);
$minify = $file_type === 'css' ? $this->cfg_css_min : $this->cfg_js_min;
$filename_info = $this->__optimizer->serve($this->_request_url, $file_type, $minify, $src_list);
return false; // Failed to generate
list($filename, $type) = $filename_info;
// Add cache tag in case later file deleted to avoid lscache served stale non-existed files @since 4.4.1
Tag::add(Tag::TYPE_MIN . '.' . $filename);
$qs_hash = substr(md5(self::get_option(self::ITEM_TIMESTAMP_PURGE_CSS)), -5);
// As filename is already related to filecon md5, no need QS anymore
$filepath_prefix = $this->_build_filepath_prefix($type);
return LITESPEED_STATIC_URL . $filepath_prefix . $filename . '?ver=' . $qs_hash;
private function _parse_js() {
$excludes = apply_filters('litespeed_optimize_js_excludes', $this->conf(self::O_OPTM_JS_EXC));
$combine_ext_inl = $this->conf(self::O_OPTM_JS_COMB_EXT_INL);
if (!apply_filters('litespeed_optm_js_comb_ext_inl', true)) {
self::debug2('js_comb_ext_inl bypassed via litespeed_optm_js_comb_ext_inl filter');
$combine_ext_inl = false;
// V7 added: (?:\r\n?|\n?) to fix replacement leaving empty new line
$content = preg_replace('#<!--.*-->(?:\r\n?|\n?)#sU', '', $this->content);
preg_match_all('#<script([^>]*)>(.*)</script>(?:\r\n?|\n?)#isU', $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$attrs = empty($match[1]) ? array() : Utility::parse_attr($match[1]);
if (isset($attrs['data-optimized'])) {
if (!empty($attrs['data-no-optimize'])) {
if (!empty($attrs['data-cfasync']) && $attrs['data-cfasync'] === 'false') {
if (!empty($attrs['type']) && $attrs['type'] != 'text/javascript') {
// to avoid multiple replacement
if (in_array($match[0], $html_list)) {
if (!empty($attrs['src'])) {
$js_excluded = Utility::str_hit_array($attrs['src'], $excludes);
$is_internal = Utility::is_internal_file($attrs['src']);
$is_file = substr($attrs['src'], 0, 5) != 'data:';
$ext_excluded = !$combine_ext_inl && !$is_internal;
if ($js_excluded || $ext_excluded || !$is_file) {
if ($this->cfg_js_defer) {
$deferred = $this->_js_defer($match[0], $attrs['src']);
$this->content = str_replace($match[0], $deferred, $this->content);
self::debug2('_parse_js bypassed due to ' . ($js_excluded ? 'js files excluded [hit] ' . $js_excluded : 'external js'));
if (strpos($attrs['src'], '/localres/') !== false) {
if (strpos($attrs['src'], 'instant_click') !== false) {
$this_src_arr['src'] = $attrs['src'];
elseif (!empty($match[2])) {
// self::debug( '🌹🌹🌹 ' . $match[2] . '🌹' );
$js_excluded = Utility::str_hit_array($match[2], $excludes);
if ($js_excluded || !$combine_ext_inl) {
if ($this->cfg_js_defer) {
$deferred = $this->_js_inline_defer($match[2], $match[1]);
$this->content = str_replace($match[0], $deferred, $this->content);
self::debug2('_parse_js bypassed due to ' . ($js_excluded ? 'js excluded [hit] ' . $js_excluded : 'inline js'));
$this_src_arr['inl'] = true;
$this_src_arr['src'] = $match[2];
$this_src_arr['attrs'] = $match[1];
// Compatibility to those who changed src to data-src already
self::debug2('No JS src or inline JS content');
$src_list[] = $this_src_arr;
$html_list[] = $match[0];
return array( $src_list, $html_list );
private function _js_inline_defer( $con, $attrs = false, $minified = false ) {
if (strpos($attrs, 'data-no-defer') !== false) {
self::debug2('bypass: attr api data-no-defer');
$hit = Utility::str_hit_array($con, $this->cfg_js_defer_exc);
self::debug2('inline js defer excluded [setting] ' . $hit);
// && $this->cfg_js_defer !== 2
$con = Optimizer::minify_js($con);
// Check if the content contains ESI nonce or not
$con = $this->_preserve_esi($con);