在网络速度较低的情况下,大量照片会导致网站加载缓慢。本文档详细介绍了优化家庭网站中照片加载速度的多种方法和技术。以下是主要的优化策略及其具体实现:
data-src
属性存储实际图片路径,并通过JavaScript实现延迟加载。IntersectionObserver
或回退方案确保图片仅在进入视口时加载。
元素支持多种格式。from PIL import Image
import io
def optimize_image(image_file, quality=85, max_width=1920):
"""压缩并优化图片"""
img = Image.open(image_file)
# 调整大小
if img.width > max_width:
ratio = max_width / img.width
new_height = int(img.height * ratio)
img = img.resize((max_width, new_height), Image.LANCZOS)
# 转换为RGB模式(如果是RGBA)
if img.mode == 'RGBA':
img = img.convert('RGB')
# 保存为优化的JPEG
output = io.BytesIO()
img.save(output, format='JPEG', quality=quality, optimize=True)
output.seek(0)
return output
将此函数集成到上传处理中:
@bp.route('/upload', methods=['POST'])
def upload_photo():
if 'photo' not in request.files:
return redirect(request.url)
file = request.files['photo']
if file.filename == '':
return redirect(request.url)
if file and allowed_file(file.filename):
# 优化图片
optimized_image = optimize_image(file)
# 保存优化后的图片
filename = secure_filename(file.filename)
unique_filename = f"{uuid.uuid4().hex}_{filename}"
file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
with open(file_path, 'wb') as f:
f.write(optimized_image.getvalue())
# 保存到数据库...
创建一个脚本批量处理现有图片:
import os
from PIL import Image
import glob
def batch_optimize_images(folder_path, quality=85, max_width=1920):
"""批量优化文件夹中的所有图片"""
image_files = glob.glob(os.path.join(folder_path, "*.jpg")) + \
glob.glob(os.path.join(folder_path, "*.jpeg")) + \
glob.glob(os.path.join(folder_path, "*.png"))
for img_path in image_files:
try:
# 创建备份
backup_path = img_path + ".backup"
if not os.path.exists(backup_path):
os.rename(img_path, backup_path)
# 优化图片
img = Image.open(backup_path)
# 调整大小
if img.width > max_width:
ratio = max_width / img.width
new_height = int(img.height * ratio)
img = img.resize((max_width, new_height), Image.LANCZOS)
# 转换为RGB模式(如果是RGBA)
if img.mode == 'RGBA':
img = img.convert('RGB')
# 保存为优化的JPEG
img.save(img_path, format='JPEG', quality=quality, optimize=True)
print(f"优化完成: {img_path}")
except Exception as e:
print(f"处理 {img_path} 时出错: {str(e)}")
# 恢复备份
if os.path.exists(backup_path):
os.rename(backup_path, img_path)
# 使用示例
batch_optimize_images("app/static/uploads/photos")
def create_thumbnail(image_path, thumbnail_size=(300, 300)):
"""为原图创建缩略图"""
img = Image.open(image_path)
img.thumbnail(thumbnail_size)
# 生成缩略图文件名
filename = os.path.basename(image_path)
thumbnail_dir = os.path.join(os.path.dirname(os.path.dirname(image_path)), "thumbnails")
os.makedirs(thumbnail_dir, exist_ok=True)
thumbnail_path = os.path.join(thumbnail_dir, filename)
# 保存缩略图
img.save(thumbnail_path, optimize=True)
return os.path.relpath(thumbnail_path, app.static_folder)
修改相册模板,使用缩略图:
{% for photo in photos %}
<div class="photo-item">
<a href="{{ url_for('static', filename='uploads/photos/' + photo.filename) }}"
data-lightbox="album-{{ album.id }}">
<img src="{{ url_for('static', filename='uploads/thumbnails/' + photo.filename) }}"
alt="{{ photo.description or '照片' }}"
class="img-thumbnail">
a>
div>
{% endfor %}
修改模板中的图片标签:
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
data-src="{{ url_for('static', filename='uploads/photos/' + photo.filename.split('/')[-1]) }}"
alt="{{ photo.description or '照片' }}"
class="lazy-load">
document.addEventListener("DOMContentLoaded", function() {
let lazyImages = [].slice.call(document.querySelectorAll("img.lazy-load"));
if ("IntersectionObserver" in window) {
let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImage.classList.remove("lazy-load");
lazyImageObserver.unobserve(lazyImage);
}
});
});
lazyImages.forEach(function(lazyImage) {
lazyImageObserver.observe(lazyImage);
});
} else {
// 回退方案
let active = false;
const lazyLoad = function() {
if (active === false) {
active = true;
setTimeout(function() {
lazyImages.forEach(function(lazyImage) {
if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
lazyImage.src = lazyImage.dataset.src;
lazyImage.classList.remove("lazy-load");
lazyImages = lazyImages.filter(function(image) {
return image !== lazyImage;
});
if (lazyImages.length === 0) {
document.removeEventListener("scroll", lazyLoad);
window.removeEventListener("resize", lazyLoad);
window.removeEventListener("orientationchange", lazyLoad);
}
}
});
active = false;
}, 200);
}
};
document.addEventListener("scroll", lazyLoad);
window.addEventListener("resize", lazyLoad);
window.addEventListener("orientationchange", lazyLoad);
}
});
修改图片保存函数,使用渐进式JPEG:
def save_photo(file):
# ... 现有代码 ...
# 使用PIL处理图片
img = Image.open(file)
if img.mode == 'RGBA':
img = img.convert('RGB')
# 保存为渐进式JPEG
img.save(file_path, 'JPEG', quality=85, optimize=True, progressive=True)
# ... 现有代码 ...
WebP格式比JPEG和PNG更小,但保持相同的质量:
def convert_to_webp(image_path, quality=85):
"""将图片转换为WebP格式"""
img = Image.open(image_path)
# 生成WebP文件名
filename = os.path.splitext(os.path.basename(image_path))[0] + '.webp'
webp_path = os.path.join(os.path.dirname(image_path), filename)
# 保存为WebP
img.save(webp_path, 'WEBP', quality=quality)
return os.path.basename(webp_path)
在模板中使用picture元素支持多种格式:
<picture>
<source srcset="{{ url_for('static', filename='uploads/photos/' + photo.webp_filename) }}" type="image/webp">
<img src="{{ url_for('static', filename='uploads/photos/' + photo.filename) }}"
alt="{{ photo.description or '照片' }}">
picture>
修改相册视图函数,支持分页:
@bp.route('/album/' )
def view_album(album_id):
album = Album.query.get_or_404(album_id)
page = request.args.get('page', 1, type=int)
per_page = 12 # 每页显示12张照片
photos = Photo.query.filter_by(album_id=album_id).paginate(page=page, per_page=per_page)
return render_template('album/view.html', album=album, photos=photos)
修改模板,添加分页控件:
<div class="row">
{% for photo in photos.items %}
{% endfor %}
div>
<nav aria-label="相册分页">
<ul class="pagination justify-content-center">
{% if photos.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('album.view_album', album_id=album.id, page=photos.prev_num) }}">上一页a>
li>
{% else %}
<li class="page-item disabled">
<span class="page-link">上一页span>
li>
{% endif %}
{% for page_num in photos.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if page_num %}
{% if page_num == photos.page %}
<li class="page-item active">
<span class="page-link">{{ page_num }}span>
li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('album.view_album', album_id=album.id, page=page_num) }}">{{ page_num }}a>
li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...span>
li>
{% endif %}
{% endfor %}
{% if photos.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('album.view_album', album_id=album.id, page=photos.next_num) }}">下一页a>
li>
{% else %}
<li class="page-item disabled">
<span class="page-link">下一页span>
li>
{% endif %}
ul>
nav>
如果您有公网服务器,可以考虑使用CDN加速:
使用免费CDN服务:
配置Nginx作为静态资源服务器:
server {
listen 80;
server_name static.yourdomain.com;
location / {
root /var/www/family_website/app/static;
expires 30d;
add_header Cache-Control "public, max-age=2592000";
gzip on;
gzip_types text/plain text/css application/javascript image/svg+xml;
gzip_min_length 1000;
}
}
然后在Flask应用中配置静态URL:
app.config['STATIC_URL'] = 'http://static.yourdomain.com/'
替代分页的另一种方式是无限滚动:
let page = 1;
let loading = false;
const container = document.querySelector('.photos-container');
function loadMorePhotos() {
if (loading) return;
loading = true;
page++;
fetch(`/api/album/${albumId}/photos?page=${page}`)
.then(response => response.json())
.then(data => {
if (data.photos.length > 0) {
data.photos.forEach(photo => {
const photoElement = document.createElement('div');
photoElement.className = 'col-md-3 mb-4';
photoElement.innerHTML = `
${photo.filename}" data-lightbox="album">
${photo.filename}"
alt="${photo.description || '照片'}"
class="img-thumbnail">
`;
container.appendChild(photoElement);
});
loading = false;
} else {
// 没有更多照片了
window.removeEventListener('scroll', handleScroll);
}
})
.catch(error => {
console.error('加载更多照片时出错:', error);
loading = false;
});
}
function handleScroll() {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 500) {
loadMorePhotos();
}
}
window.addEventListener('scroll', handleScroll);
添加相应的API端点:
@bp.route('/api/album//photos' )
def api_album_photos(album_id):
page = request.args.get('page', 1, type=int)
per_page = 12
photos = Photo.query.filter_by(album_id=album_id).paginate(page=page, per_page=per_page)
return jsonify({
'photos': [{
'id': photo.id,
'filename': photo.filename,
'description': photo.description
} for photo in photos.items],
'has_next': photos.has_next
})
对于相册浏览,可以预加载下一张图片:
function preloadImages() {
const images = document.querySelectorAll('a[data-lightbox="album"]');
let currentIndex = 0;
// 预加载当前可见图片的下一张
images.forEach((image, index) => {
if (isElementInViewport(image)) {
currentIndex = index;
}
});
// 预加载下一张图片
if (currentIndex + 1 < images.length) {
const nextImage = new Image();
nextImage.src = images[currentIndex + 1].href;
}
}
function isElementInViewport(el) {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
window.addEventListener('scroll', preloadImages);
document.addEventListener('DOMContentLoaded', preloadImages);
在Nginx配置中添加:
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
application/javascript
application/json
application/x-javascript
application/xml
image/svg+xml
text/css
text/javascript
text/plain
text/xml;
更新Nginx配置以支持HTTP/2:
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 其他配置...
}
首先实施最简单的优化:
根据网站规模选择合适的方案:
监控性能改进:
使用浏览器开发工具的Network面板监控加载时间
通过实施这些优化,您的家庭网站在低速网络环境下的照片加载速度应该会有显著提升。