PHP缩略图图像生成器缓存:如何在PHP中正确设置If-Last-Modified / Max-Age / Last-Modified HEADERS?

时间:2011-03-11 20:25:25

标签: php .htaccess caching image-processing

即使获得了非常高分的Google PageSpeed( 97 )&雅虎YSlow( 92 )PHP生成的缩略图似乎没有从旧缓存中被动地传递:它们似乎每次都会生成......而且......再次......新鲜出炉消耗了大量的腰部时间。

这个问题只关注&特别是关于如何解决生成拇指的PHP代码的CACHE问题:

看看这些微小的微小缩略图,每个只有3~5 kb!

  

详细瀑布:http://www.webpagetest.org/result/110328_AM_8T00/1/details/

任何&所有建议都是+1给我的帮助,并热烈欢迎,因为我已经在这个问题上变得非常绝望了几个月。 Thanx a Thousand!

使用或不使用Modrewrite不影响速度:两者都相同。我使用这些重写条件:RewriteCond %{REQUEST_URI} ^/IMG-.*$& RewriteCond %{REQUEST_FILENAME} !-f

original default URLbeautified rewritten URL都会产生相同的延迟!因此,让我们不要将错误指向闪电般快速的Apache:它的PHP Cache /标题以某种方式错误编码......

enter image description here


webpagetest.org警告:利用静态资产的浏览器缓存:69/100

失败 - (无最高年龄或过期):http://aster.nu/imgcpu?src=aster_bg/124.jpg&w=1400&h=100&c=p


每次刷新后,您都会看到two warnings appear on random at REDbot.org中的任何一个 enter image description here enter image description here


守则的相关部分:

// Script is directly called
if(isset($_GET['src']) && (isset($_GET['w']) || isset($_GET['h']) || isset($_GET['m']) || isset($_GET['f']) || isset($_GET['q']))){
    $ImageProcessor = new ImageProcessor(true);
    $ImageProcessor->Load($_GET['src'], true);
    $ImageProcessor->EnableCache("/var/www/vhosts/blabla.org/httpdocs/tmp/", 345600);
    $ImageProcessor->Parse($quality);
}

/* Images processing class
 * - create image thumbnails on the fly
 * - Can be used with direct url imgcpu.php?src=
 * - Cache images for efficiency 
 */
class ImageProcessor
{
    private $_image_path;      # Origninal image path
    protected $_image_name;    # Image name   string
    private $_image_type;      # Image type  int    
    protected $_mime;          # Image mime type  string    
    private $_direct_call = false;   # Is it a direct url call?  boolean        
    private $_image_resource;  # Image resource   var Resource      
    private $_cache_folder;    # Cache folder strig
    private $_cache_ttl;        # Cache time to live  int
    private $_cache = false;    # Cache on   boolean
    private $_cache_skip = false;   # Cache skip   var boolean

    private function cleanUrl($image){   # Cleanup url
        $cimage = str_replace("\\", "/", $image);
        return $cimage;
    }   

    /** Get image resource
     * @access private, @param string $image, @param string $extension, @return resource  */
    private function GetImageResource($image, $extension){
        switch($extension){
            case "jpg":
                @ini_set('gd.jpeg_ignore_warning', 1);
                $resource = imagecreatefromjpeg($image);
                break;
        }
        return $resource;
    }


    /* Save image to cache folder
     * @access private, @return void  */
    private function cacheImage($name, $content){

        # Write content file
        $path = $this->_cache_folder . $name;
        $fh = fopen($path, 'w') or die("can't open file");
        fwrite($fh, $content);
        fclose($fh);

        # Delete expired images
        foreach (glob($this->_cache_folder . "*") as $filename) {
            if(filemtime($filename) < (time() - $this->_cache_ttl)){
                unlink( $filename );
            }
        }
    }

    /* Get an image from cache
     * @access public, @param string $name, @return void */
    private function cachedImage($name){
        $file = $this->_cache_folder . $name;
        $fh = fopen($file, 'r');
        $content = fread($fh,  filesize($file));
        fclose($fh);
        return $content;
    }

    /* Get name of the cache file
     * @access private, @return string  */
    private function generateCacheName(){
        $get = implode("-", $_GET);
        return md5($this->_resize_mode . $this->_image_path . $this->_old_width . $this->_old_height . $this->_new_width . $this->_new_height . $get) . "." . $this->_extension;
    }

    /* Check if a cache file is expired
     * @access private,  @return bool  */
    private function cacheExpired(){
        $path = $this->_cache_folder . $this->generateCacheName();
        if(file_exists($path)){
            $filetime = filemtime($path);
            return $filetime < (time() - $this->_cache_ttl);
        }else{
            return true;
        }
    }

    /* Lazy load the image resource needed for the caching to work
     * @return void */
    private function lazyLoad(){
        if(empty($this->_image_resource)){
            if($this->_cache && !$this->cacheExpired()){
                $this->_cache_skip = true;
                return;
            }
            $resource = $this->GetImageResource($this->_image_path, $this->_extension);
            $this->_image_resource = $resource;
        }    
    }

    /* Constructor
     * @access public, @param bool $direct_call, @return void */
    public function __construct($direct_call=false){

    # Check if GD extension is loaded
        if (!extension_loaded('gd') && !extension_loaded('gd2')) {
            $this->showError("GD is not loaded");
        }

        $this->_direct_call = $direct_call;
    }

    /* Resize
     * @param int $width, @param int $height, @param define $mode
     * @param bool $auto_orientation houd rekening met orientatie wanneer er een resize gebeurt */
    public function Resize($width=100, $height=100, $mode=RESIZE_STRETCH, $auto_orientation=false){

        // Validate resize mode
        $valid_modes = array("f", "p");
        }
                     // .... omitted .....

        // Set news size vars because these are used for the
        // cache name generation
                 // .... omitted .....          
        $this->_old_width = $width;
        $this->_old_height = $height;

        // Lazy load for the directurl cache to work
        $this->lazyLoad();
        if($this->_cache_skip) return true;

        // Create canvas for the new image
        $new_image = imagecreatetruecolor($width, $height);

        imagecopyresampled($new_image, $this->_image_resource, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);

             // .... omitted .....

        $this->_image_resource = $new_image;
    }

    /* Create image resource from path or url
     * @access public, @param string $location, @param bool $lazy_load, @return */
    public function Load($image,$lazy_load=false){

        // Cleanup image url
        $image = $this->cleanUrl($image);

        // Check if it is a valid image
        if(isset($mimes[$extension]) && ((!strstr($image, "http://") && file_exists($image)) || strstr($image, "http://")) ){

            // Urlencode if http
            if(strstr($image, "http://")){
                $image = str_replace(array('http%3A%2F%2F', '%2F'), array('http://', '/'), urlencode($image));
            }
            $image = str_replace("+", "%20", $image);

            $this->_extension = $extension;
            $this->_mime = $mimes[$extension];
            $this->_image_path = $image;
            $parts = explode("/", $image);
            $this->_image_name = str_replace("." . $this->_extension, "", end($parts));

            // Get image size
            list($width, $height, $type) = getimagesize($image);
            $this->_old_width = $width;
            $this->_old_height = $height;
            $this->_image_type = $type;
        }else{
            $this->showError("Wrong image type or file does not exists.");
        }
        if(!$lazy_load){
            $resource = $this->GetImageResource($image, $extension);
            $this->_image_resource = $resource;
        }           
    }

    /* Save image to computer
     * @access public, @param string $destination, @return void  */
    public function Save($destination, $quality=60){
        if($this->_extension == "png" || $this->_extension == "gif"){
            imagesavealpha($this->_image_resource, true); 
        }
        switch ($this->_extension) {
            case "jpg": imagejpeg($this->_image_resource,$destination, $quality);   break;
            case "gif": imagegif($this->_image_resource,$destination);      break;
            default: $this->showError('Failed to save image!');             break;
        }           
    }

    /* Print image to screen
     * @access public, @return void */
    public function Parse($quality=60){
        $name = $this->generateCacheName();
        $content = "";
        if(!$this->_cache || ($this->_cache && $this->cacheExpired())){
            ob_start();
            header ("Content-type: " . $this->_mime);
            if($this->_extension == "png" || $this->_extension == "gif"){
                imagesavealpha($this->_image_resource, true); 
            }

            switch ($this->_extension) {
                case "jpg": imagejpeg($this->_image_resource, "", $quality);    break;
                case "gif": imagegif($this->_image_resource);   break;
                default: $this->showError('Failed to save image!');             break;
            }

            $content = ob_get_contents();
            ob_end_clean();
        }else{

            if (isset ($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
                if (strtotime ($_SERVER['HTTP_IF_MODIFIED_SINCE']) < strtotime('now')) {
                    header ('HTTP/1.1 304 Not Modified');
                    die ();
                }
            }

            // change the modified headers
            $gmdate_expires = gmdate ('D, d M Y H:i:s', strtotime ('now +10 days')) . ' GMT';
            $gmdate_modified = gmdate ('D, d M Y H:i:s') . ' GMT';

            header ("Content-type: " . $this->_mime);
            header ('Accept-Ranges: bytes');
            header ('Last-Modified: ' . $gmdate_modified);
            header ('Cache-Control: max-age=864000, must-revalidate');
            header ('Expires: ' . $gmdate_expires);

            echo $this->cachedImage($name);
            exit();
        }

        // Save image content
        if(!empty($content) && $this->_cache){
            $this->cacheImage($name, $content);
        }

        // Destroy image
        $this->Destroy();

        echo $content;
        exit();
    }

    /* Destroy resources
     * @access public,  @return void */
    public function Destroy(){
        imagedestroy($this->_image_resource); 
    }


    /* Get image resources
     * @access public,  @return resource */
    public function GetResource(){
        return $this->_image_resource;
    }

    /* Set image resources
     * @access public, @param resource $image, @return resource */
    public function SetResource($image){
        $this->_image_resource = $image;
    }

    /* Enable caching
     * @access public, @param string $folder, @param int $ttl,   * @return void */
    public function EnableCache($folder="/var/www/vhosts/blabla.org/httpdocs/tmp/", $ttl=345600){
        if(!is_dir($folder)){
            $this->showError("Directory '" . $folder . "' does'nt exist");
        }else{
            $this->_cache           = true;
            $this->_cache_folder    = $folder;
            $this->_cache_ttl       = $ttl;
        }
        return false;
    }
}
  

原作者允许我在此处放置部分代码以解决此问题。


5 个答案:

答案 0 :(得分:5)

如果我正确地理解了这个问题,那完全可以预料到。图像处理很慢。

黄色是您的浏览器发送请求。绿色是您的浏览器在服务器上等待实际创建缩略图,这需要非常大的时间,无论服务器使用什么库。蓝色是发送响应的服务器,与前面的步骤不同,它受文件大小的影响。

关于图像处理的固有慢度,没有太多的事要做。将这些缩略图缓存以便它们仅生成一次然后静态提供是明智的。这样,很少有用户可以忍受绿色延迟,您的服务器也会很高兴。

编辑:如果问题是文件存在于这些网址中,但您的RewriteRule仍在冒险,请记住,默认情况下,规则会运行而不检查文件存在。

RewriteRule上方使用以下条件以确保文件存在。

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule # ...etc...

答案 1 :(得分:3)

  

imgcpu.php SRC =富/ foo.jpg&安培; W = 100安培; H = 100

所以imgcpu.php正在为每个图片请求运行?

在这种情况下,如果您担心表现,请:

  • 脚本需要对其创建的缩略图进行一些缓存。如果它在每个请求上调整大小,那就是你的问题。

  • 脚本需要向浏览器发送一些缓存标头 - 纯PHP脚本不会这样做,并且会在每次加载页面时刷新

  • PHP脚本中的session_start()调用可能会因会话锁定而导致并发问题。

您需要显示一些PHP代码。但也许在一个单独的问题中。

答案 2 :(得分:3)

Apache可以比你的硬盘更快地提供硬盘上的文件,看来你正在做后者来处理缓存:

 /**
     * Get an image from cache
     * 
     * @access public
     * @param string $name
     * @return void
     */
    private function cachedImage($name){
        $file = $this->_cache_folder . $name;
        $fh = fopen($file, 'r');
        $content = fread($fh,  filesize($file));
        fclose($fh);
        return $content;
    }

有一种更好的方法来执行该函数正在执行的操作(passthru),但最好的选择是设置一个正则表达式,只有当文件尚不存在时才会重写对缩略图脚本的请求:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^images/.*$ - [NC,L]
RewriteRule ^images/(.*)$ /imgcpu.php/$1 [NC,L]

然后引入逻辑来解析对图像的请求并相应地格式化它。例如,您可以说拇指应该以原始文件命名,并且附加W x H尺寸,如“stackoverflow_logo_100x100.jpg”。

有意义吗?


每个请求(在注释中),“s”,“l”和“d”标志的描述如下(引用文档):

  

' - d'(是目录)对待   TestString作为路径名并进行测试   它是否存在,是一个   。目录

     

' - s'(是   常规文件,大小)对待   TestString作为路径名并进行测试   它是否存在,是一个   大小超过的常规文件   零。

     

' - l'(是符号链接)对待   TestString作为路径名并进行测试   它是否存在,是一个   符号链接。

答案 3 :(得分:2)

在生成图像后检查HTTP_IF_MODIFIED_SINCE标头和缓存,以便每次加载页面时生成并缓存图像。如果在开始处理图像之前将这些检查移近执行开始,则会大大减少时间。

答案 4 :(得分:0)

Matchu给你答案为什么。如果要修复它,请保存创建的缩略图,这样就不会在每个请求中重新创建它们。我使用简单的404页面来捕获尚未创建的缩略图请求,该脚本从url中找出所需的尺寸和文件 - 例如/thumbs/100x100/cat.png表示从/images/cat.png创建100x100缩略图。