我是Rails新手,刚刚读完Michael Hartl的优秀Rails教程,我正在开发另一个项目应用程序,它松散地遵循这个框架,但有不同之处。
我已经做了一部分,已经遇到了一个问题,我的应用程序的许多部分似乎异常缓慢,我不认为一个明显的原因。我把我的用户索引页的细节放在下面,因为这是最慢的。如下所示,它需要9.1秒加载,其中约8.7秒加载视图。这是在开发环境(AWS Cloud9),但即使部署在Heroku上,渲染数据库中只有13个用户的页面也需要~ 5秒!
5s在这个有限的规模下是相当糟糕的,并且rails教程示例应用程序在显示50个用户的情况下总共在447 ms内呈现用户索引页面!(运行在类似的AWS EC2示例上)我不认为我的应用程序有什么不同,足以导致加载时间增加20倍。
渲染/用户服务器输出:
Started GET "/users" for 24.85.170.222 at 2023-01-29 21:52:03 +0000
Cannot render console from 24.85.170.222! Allowed networks: 127.0.0.0/127.255.255.255, ::1
Processing by UsersController#index as HTML
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
↳ app/helpers/sessions_helper.rb:12:in `current_user'
Rendering layout layouts/application.html.erb
Rendering users/index.html.erb within layouts/application
User Count (1.1ms) SELECT COUNT(*) FROM "users"
↳ app/views/users/index.html.erb:5
User Load (0.6ms) SELECT "users".* FROM "users" LIMIT ? OFFSET ? [["LIMIT", 30], ["OFFSET", 0]]
↳ app/views/users/index.html.erb:8
Rendered users/index.html.erb within layouts/application (Duration: 7719.3ms | Allocations: 280179)
Rendered layouts/_rails_default.html.erb (Duration: 7.6ms | Allocations: 6444)
Rendered layouts/_shim.html.erb (Duration: 0.0ms | Allocations: 9)
Company Load (0.1ms) SELECT "companies".* FROM "companies" INNER JOIN "company_users" ON "companies"."id" = "company_users"."company_id" WHERE "company_users"."user_id" = ? AND "company_users"."employee" = ? LIMIT ? [["user_id", 2], ["employee", 1], ["LIMIT", 1]]
↳ app/views/layouts/_header.html.erb:35
Rendered layouts/_header.html.erb (Duration: 963.3ms | Allocations: 1473)
Rendered layouts/_footer.html.erb (Duration: 0.2ms | Allocations: 73)
Rendered layout layouts/application.html.erb (Duration: 8693.9ms | Allocations: 289996)
Completed 200 OK in 9147ms (Views: 8697.1ms | ActiveRecord: 2.2ms | Allocations: 291862)
用户_控制器.rb:
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :show]
before_action :correct_user, only: [:edit, :update]
def index
@users = User.paginate(page: params[:page])
end
end
/型号/用户.rb:
class User < ApplicationRecord
before_save :downcase_email
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
#Returns true if given token matches the digest
def authenticated?(token)
#debugger
return false if session_digest.nil?
BCrypt::Password.new(session_digest).is_password?(token)
end
# Returns the hash digest of the given string.
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
#returns a session token to prevent session hijacking
def set_session_token
@session_token = SecureRandom.urlsafe_base64
update_attribute(:session_digest, User.digest(@session_token))
return @session_token
end
private
def downcase_email
email.downcase!
end
end
会话_助手.rb:
module SessionsHelper
def log_in(user)
session[:user_id] = user.id
#guard against session replay attacks
session[:session_token] = user.set_session_token
end
def current_user
if (user_id = session[:user_id])
@sess_user ||= User.find_by(id: user_id)
if @sess_user && @sess_user.authenticated?(session[:session_token])
@current_user = @sess_user
end
end
end
def current_user?(user)
user && user == current_user
end
def logged_in?
!current_user.nil?
end
#confirms a logged in user
def logged_in_user
unless logged_in?
store_location #store requested URL in a cookie for friendly forwarding
flash[:danger] = "Please log in"
redirect_to root_url, status: :see_other
end
end
end
/视图/用户/索引.html.erb:
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= link_to user.display_name, user %>
<% if current_user.superadmin? && !current_user?(user) %>
| <%= link_to "delete", user, data: { "turbo-method": :delete, turbo_confirm: "Delete #{user.email}?" } %>
<% end %>
</li>
<% end %>
</ul>
<%= will_paginate %>
视图/布局/应用程序.html.erb:
<!DOCTYPE html>
<html>
<head>
<title><%= full_title(yield(:title)) %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta charset="utf-8">
<%= render 'layouts/rails_default' %>
<%= render 'layouts/shim' %>
<%= javascript_importmap_tags %>
</head>
<body>
<% flash.each do |message_type, message| %>
<%= content_tag(:div, message, class: "alert alert-#{message_type}") %>
<% end %>
<%= render 'layouts/header' %>
<div class="container">
<%= yield %>
<%= render 'layouts/footer' %>
<%= debug(params) if Rails.env.development? %>
</div>
</body>
</html>
我的视图/布局/_header.html.erb文件也有几次命中is_logged_in?
和current_user
。
我使用的是与railstutorial.org示例应用程序相同的Gem,所以Gem不应该是问题所在。
一开始我以为current_user
会根据日志多次访问数据库,所以我用@sess_user ||= User.find_by(id: user_id)
行来记住它,但这几乎没有影响加载时间,当我与示例应用程序比较时,我意识到这不是因为数据库访问被缓存了:CACHE User Load (0.0ms)
我觉得有一些明显的东西我错过了。我已经阅读了N+1查询和使用.includes
,特别是在我的网站上有一些其他的模型,这可能是相关的,但事实上,即使在非常普通的用户索引页面上也会出现这个问题,这让我感到困惑。
我仍然只是开发的一部分,但是我想在我走得太远和事情变得更复杂之前尝试和弄清楚这个问题。将非常感谢任何见解!
1条答案
按热度按时间czq61nw11#
Alex对这个问题的第一个评论直指问题的核心。我如下所示重构了current_user以记住返回值,这样每个视图只调用一次is_password?。即时缓解-页面加载时间现在低于400 ms。
这都归功于亚历克斯。他做到了。他的评论真的应该是“答案”,但我觉得我应该把这个贴在这里,这样问题就显示为“回答”了。